Эх сурвалжийг харах

移动云盘业务,相册授权

nelson 3 сар өмнө
parent
commit
1dd495107b
94 өөрчлөгдсөн 3880 нэмэгдсэн , 109 устгасан
  1. 1 0
      pom.xml
  2. 3 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/McdiskApplication.java
  3. 51 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/config/FeignConfiguration.java
  4. 30 4
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/authorizephoto/AppAuthorizePhotoController.java
  5. 3 3
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/authorizephoto/vo/AppOauthPhotoReqVO.java
  6. 6 2
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/authorizephoto/vo/AuthorizePhotoRespVO.java
  7. 27 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/authorizephoto/vo/NotifyWatchAuthorizationReqVO.java
  8. 9 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/authorizephoto/vo/QueryOauthPhotoByDeviceIdRespVO.java
  9. 75 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/websocket/AppMessageController.java
  10. 38 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/websocket/WristWatchMessageController.java
  11. 25 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/websocket/vo/ReloginOnWwatchReqVO.java
  12. 19 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/wristwatch/AuthorizeController.java
  13. 4 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/wristwatch/vo/AuthorizeRespVO.java
  14. 20 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/wristwatch/vo/LogoutReqVO.java
  15. 5 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/dal/dataobject/AuthorizeDO.java
  16. 2 6
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/dal/dataobject/AuthorizePhotoDO.java
  17. 10 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/dal/mysql/AuthorizeMapper.java
  18. 3 5
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/dal/mysql/AuthorizePhotoMapper.java
  19. 24 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/ActivateCloudPhotoAlbumMessage.java
  20. 49 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/BasicMessage.java
  21. 0 39
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/FamilyCloudList.java
  22. 15 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/MessagingPublishResponse.java
  23. 27 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/NotifyParentsSuccessfulActivationMessage.java
  24. 28 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/ReloginOnWwatchMessage.java
  25. 7 3
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/enums/CodeStatusEnum.java
  26. 1 1
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/enums/IsAuthorizeEnum.java
  27. 53 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/enums/MessageType.java
  28. 47 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/feign/ApiMessageFeignService.java
  29. 52 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/feign/fallback/ApiMessageFallbackFactory.java
  30. 2 5
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/AuthorizePhotoService.java
  31. 48 30
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/AuthorizePhotoServiceImpl.java
  32. 2 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/AuthorizeService.java
  33. 47 6
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/AuthorizeServiceImpl.java
  34. 23 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/MessageIdIncrementingService.java
  35. 39 0
      sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/MessageIdIncrementingServiceImpl.java
  36. 2 2
      sikey-selenium-business/sikey-selenium-business-biz/src/main/java/cn/sikey/selenium/api/SmsServiceImpl.java
  37. 22 0
      sikey-selenium-business/sikey-selenium-business-biz/src/main/java/cn/sikey/selenium/scheduler/KeepItAliveScheduler.java
  38. 10 3
      sikey-selenium-business/sikey-selenium-business-biz/src/main/java/cn/sikey/selenium/util/WebDriverContextManagerUtil.java
  39. 292 0
      sikey-websocket-business/pom.xml
  40. 128 0
      sikey-websocket-business/sikey-websocket-business-api/pom.xml
  41. 0 0
      sikey-websocket-business/sikey-websocket-business-api/src/main/java/cn/sikey/websocket/api/test.json
  42. 23 0
      sikey-websocket-business/sikey-websocket-business-api/src/main/java/cn/sikey/websocket/enums/ApiConstants.java
  43. 24 0
      sikey-websocket-business/sikey-websocket-business-biz/Dockerfile
  44. 371 0
      sikey-websocket-business/sikey-websocket-business-biz/pom.xml
  45. 25 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/WebsocketServerApplication.java
  46. 76 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/client/handler/MessageQueueHandler.java
  47. 99 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/client/handler/WebSocketHandler.java
  48. 28 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/client/interceptor/WebSocketInterceptor.java
  49. 180 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/config/CacheConnectionStateManager.java
  50. 34 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/config/MessagingRabbitmqConfig.java
  51. 36 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/config/RabbitMQMessaging.java
  52. 33 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/config/ThreadPoolConfig.java
  53. 31 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/config/WebSocketConfig.java
  54. 37 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/message/MessageController.java
  55. 55 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/message/vo/MessagingPublishReqVO.java
  56. 20 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/message/vo/MessagingPublishRespVO.java
  57. 50 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/message/vo/SaveMessageRecvReqVO.java
  58. 37 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/message/vo/SaveMessageSendReqVO.java
  59. 0 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/test.json
  60. 0 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/test.json
  61. 24 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/convert/MessageConvert.java
  62. 57 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/dal/dataobject/MessageRecvDO.java
  63. 44 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/dal/dataobject/MessageSendDO.java
  64. 28 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/dal/mysql/MessageRecvMapper.java
  65. 28 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/dal/mysql/MessageSendMapper.java
  66. 52 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/enums/MessageStatus.java
  67. 51 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/enums/MessageType.java
  68. 57 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/BasicMessage.java
  69. 18 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/HeartbeatMessage.java
  70. 64 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/Message.java
  71. 26 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/MessageId.java
  72. 41 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/MessageReceivedLog.java
  73. 34 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/TestTextMessage.java
  74. 23 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/scheduler/KeepItAliveScheduler.java
  75. 125 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/AbstractWebSocketService.java
  76. 23 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageIdIncrementingService.java
  77. 39 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageIdIncrementingServiceImpl.java
  78. 14 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageRecvService.java
  79. 46 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageRecvServiceImpl.java
  80. 16 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageSendService.java
  81. 50 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageSendServiceImpl.java
  82. 13 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessagingPublishService.java
  83. 15 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessagingPublishServiceImpl.java
  84. 12 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessagingSubscribeService.java
  85. 15 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessagingSubscribeServiceImpl.java
  86. 47 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/util/ConnectionConverterUtil.java
  87. 52 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/util/MessageConverterUtil.java
  88. 44 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/util/ObjectMapperUtil.java
  89. 15 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/websocket/Ack.java
  90. 44 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/websocket/Connection.java
  91. 55 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/websocket/ConnectionHeartbeatService.java
  92. 69 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/websocket/ConnectionManagerService.java
  93. 155 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/resources/application.yml
  94. 76 0
      sikey-websocket-business/sikey-websocket-business-biz/src/main/resources/logback-spring-test.xml

+ 1 - 0
pom.xml

@@ -14,6 +14,7 @@
         <module>sikey-tools-business</module>
         <module>sikey-mcdisk-business</module>
         <module>sikey-selenium-business</module>
+        <module>sikey-websocket-business</module>
     </modules>
 
     <artifactId>sikey-business</artifactId>

+ 3 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/McdiskApplication.java

@@ -1,10 +1,12 @@
 package cn.sikey.mcdisk;
 
 
+import cn.sikey.selenium.api.selenium.SmsApi;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.openfeign.EnableFeignClients;
 
 /**
  * 项目的启动类
@@ -15,6 +17,7 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 @SpringBootApplication(exclude = {
         org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration.class
 })
+@EnableFeignClients
 @EnableDiscoveryClient
 @Slf4j
 public class McdiskApplication {

+ 51 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/config/FeignConfiguration.java

@@ -0,0 +1,51 @@
+/*
+package cn.sikey.mcdisk.config;
+
+import feign.RequestInterceptor;
+import feign.RequestTemplate;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.Enumeration;
+import java.util.Objects;
+
+*/
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: feign配置
+ *//*
+
+@Slf4j
+@Configuration
+public class FeignConfiguration implements RequestInterceptor {
+
+    */
+/**
+     * feign调用
+     *
+     * @param template
+     *//*
+
+    @Override
+    public void apply(RequestTemplate template) {
+        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (Objects.isNull(servletRequestAttributes)) {
+            return;
+        }
+        HttpServletRequest request = servletRequestAttributes.getRequest();
+        Enumeration<String> headerNames = request.getHeaderNames();
+        if (Objects.nonNull(headerNames)) {
+            while (headerNames.hasMoreElements()) {
+                String name = headerNames.nextElement();
+                String values = request.getHeader(name);
+                template.header(name, values);
+            }
+        }
+    }
+
+}
+*/

+ 30 - 4
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/authorizephoto/AppAuthorizePhotoController.java

@@ -3,10 +3,13 @@ package cn.sikey.mcdisk.controller.app.authorizephoto;
 import cn.sikey.framework.common.pojo.CommonResult;
 import cn.sikey.mcdisk.controller.app.authorizephoto.vo.AppOauthPhotoReqVO;
 import cn.sikey.mcdisk.controller.app.authorizephoto.vo.AuthorizePhotoRespVO;
+import cn.sikey.mcdisk.controller.app.authorizephoto.vo.NotifyWatchAuthorizationReqVO;
 import cn.sikey.mcdisk.controller.app.authorizephoto.vo.OauthPhotoReqVO;
+import cn.sikey.mcdisk.controller.app.wristwatch.vo.AuthorizeRespVO;
 import cn.sikey.mcdisk.enums.IsAuthorizeEnum;
 import cn.sikey.mcdisk.enums.RateLimit;
 import cn.sikey.mcdisk.service.AuthorizePhotoService;
+import cn.sikey.mcdisk.service.AuthorizeService;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
@@ -27,23 +30,33 @@ public class AppAuthorizePhotoController {
     @Resource
     private AuthorizePhotoService authorizePhotoService;
 
+    @Resource
+    private AuthorizeService authorizeService;
+
     /**
      * 查询授权认证
      *
      * @param oauthPhotoReqVO 查询授权认证
      * @return
      */
-    @RateLimit(key = "queryAuthorization", type = RateLimit.LimitType.GLOBAL)
+    // @RateLimit(key = "queryAuthorization", type = RateLimit.LimitType.GLOBAL)
     @GetMapping("/queryPhotoAuthorization")
     public CommonResult queryPhotoAuthorization(@Validated OauthPhotoReqVO oauthPhotoReqVO) {
         AuthorizePhotoRespVO authorizePhotoRespVO = authorizePhotoService.queryOauthPhoto(oauthPhotoReqVO);
         if (Objects.isNull(authorizePhotoRespVO)) {
             AuthorizePhotoRespVO respVO = new AuthorizePhotoRespVO();
-            respVO.setAuthorizeStatus(IsAuthorizeEnum.UNAUTHORIZED.getValue());
+            respVO.setLoginStatus(IsAuthorizeEnum.UNAUTHORIZED.getValue());
+            respVO.setAuthorize(IsAuthorizeEnum.UNAUTHORIZED.getValue());
             return CommonResult.success(respVO);
-        }
+        } else {
+            authorizePhotoRespVO.setAuthorize(IsAuthorizeEnum.AUTHORIZED.getValue());
+            AuthorizeRespVO queryAuthorize = authorizeService.queryAuthorize(oauthPhotoReqVO.getTicket());
+            if(Objects.nonNull(queryAuthorize)) {
+                authorizePhotoRespVO.setLoginStatus(queryAuthorize.getAuthorizeStatus());
+            }
 
-        return CommonResult.success(authorizePhotoRespVO);
+            return CommonResult.success(authorizePhotoRespVO);
+        }
     }
 
     /**
@@ -59,4 +72,17 @@ public class AppAuthorizePhotoController {
         return CommonResult.success();
     }
 
+    /**
+     * 通知手表开通云相册服务
+     *
+     * @param notifyWatchAuthorizationReqVO 通知手表授权
+     * @return
+     */
+    @RateLimit(key = "notifyWatchActivateService", type = RateLimit.LimitType.GLOBAL)
+    @PostMapping("/notifyWatchActivateService")
+    public CommonResult notifyWatchAuthorization(@Validated @RequestBody NotifyWatchAuthorizationReqVO notifyWatchAuthorizationReqVO) {
+        authorizePhotoService.notifyWatchAuthorization(notifyWatchAuthorizationReqVO);
+        return CommonResult.success();
+    }
+
 }

+ 3 - 3
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/authorizephoto/vo/AppOauthPhotoReqVO.java

@@ -35,8 +35,8 @@ public class AppOauthPhotoReqVO {
     private String nickName;
 
     /**
-     * 设备名称
+     * 型号
      */
-    @NotBlank(message = "设备名称是空")
-    private String deviceName;
+    @NotBlank(message = "型号是空")
+    private String model;
 }

+ 6 - 2
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/authorizephoto/vo/AuthorizePhotoRespVO.java

@@ -26,7 +26,11 @@ public class AuthorizePhotoRespVO {
      */
     private String ticket;
     /**
-     * 授权状态:1已授权2已创建
+     * 手表状态:0未开通1已开通登录2已开通退出登录
      */
-    private Integer authorizeStatus;
+    private Integer loginStatus;
+    /**
+     * 授权状态:0未授权1已授权
+     */
+    private Integer authorize;
 }

+ 27 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/authorizephoto/vo/NotifyWatchAuthorizationReqVO.java

@@ -0,0 +1,27 @@
+package cn.sikey.mcdisk.controller.app.authorizephoto.vo;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/3
+ * @Description: 通知手表授权
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class NotifyWatchAuthorizationReqVO {
+    /**
+     * ticket
+     */
+    @NotBlank(message = "ticket是空")
+    private String ticket;
+    /**
+     * 用户id
+     */
+    @NotBlank(message = "用户id是空")
+    private String userId;
+}

+ 9 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/authorizephoto/vo/QueryOauthPhotoByDeviceIdRespVO.java

@@ -17,6 +17,10 @@ public class QueryOauthPhotoByDeviceIdRespVO {
      * id
      */
     private Long id;
+    /**
+     * 用户id
+     */
+    private String userId;
     /**
      * 昵称
      */
@@ -25,4 +29,9 @@ public class QueryOauthPhotoByDeviceIdRespVO {
      * 设备名称
      */
     private String deviceName;
+
+    /**
+     * 相册授权状态
+     */
+    private Integer authorizeStatus;
 }

+ 75 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/websocket/AppMessageController.java

@@ -0,0 +1,75 @@
+package cn.sikey.mcdisk.controller.app.websocket;
+
+import cn.sikey.framework.common.pojo.CommonResult;
+import cn.sikey.mcdisk.controller.app.websocket.vo.ReloginOnWwatchReqVO;
+import cn.sikey.mcdisk.entity.ActivateCloudPhotoAlbumMessage;
+import cn.sikey.mcdisk.entity.ReloginOnWwatchMessage;
+import cn.sikey.mcdisk.enums.MessageType;
+import cn.sikey.mcdisk.feign.ApiMessageFeignService;
+import cn.sikey.mcdisk.service.AuthorizeService;
+import cn.sikey.mcdisk.service.MessageIdIncrementingService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.Instant;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 云相册
+ */
+@RestController
+@RequestMapping("/mcdisk/app/message")
+@Slf4j
+public class AppMessageController {
+
+    @Resource
+    private ApiMessageFeignService apiMessageFeignService;
+
+    @Resource
+    private MessageIdIncrementingService<Long> messageIdIncrementingService;
+
+    @Resource
+    private AuthorizeService authorizeService;
+
+    /**
+     * 通知手表重新登录
+     *
+     * @param request 请求
+     * @return
+     */
+    @PostMapping("/reloginWatch")
+    public CommonResult notifyReloginOnWwatch(@Validated @RequestBody ReloginOnWwatchReqVO request) {
+        ReloginOnWwatchMessage rt = new ReloginOnWwatchMessage();
+        rt.setMsgId(messageIdIncrementingService.incr());
+        rt.setMsgType(MessageType.RELOGIN_ON_WWATCH);
+        rt.setSendId(request.getUserId());
+        rt.setRecvId(request.getTicket());
+        ReloginOnWwatchMessage.Content content = new ReloginOnWwatchMessage.Content();
+        content.setUserId(request.getUserId());
+        rt.setContent(content);
+        rt.setSendTime(Instant.now().toEpochMilli());
+
+        // 更新设备授权状态
+        // authorizeService.updateAuthorizeByDeviceId(request.getTicket());
+
+        return CommonResult.success(apiMessageFeignService.reloginOnWwatch(rt));
+    }
+
+    /**
+     * 开通云相册服务通知手表
+     *
+     * @param request 请求
+     * @return
+     */
+    /*@PostMapping("/activateCloudPhotoAlbum")
+    public CommonResult activateCloudPhotoAlbum(@Validated @RequestBody ActivateCloudPhotoAlbumMessage request) {
+        return CommonResult.success(apiMessageFeignService.activateCloudPhotoAlbum(request));
+    }*/
+
+}

+ 38 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/websocket/WristWatchMessageController.java

@@ -0,0 +1,38 @@
+package cn.sikey.mcdisk.controller.app.websocket;
+
+import cn.sikey.framework.common.pojo.CommonResult;
+import cn.sikey.mcdisk.entity.NotifyParentsSuccessfulActivationMessage;
+import cn.sikey.mcdisk.feign.ApiMessageFeignService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 云相册
+ */
+@RestController
+@RequestMapping("/mcdisk/app/message")
+@Slf4j
+public class WristWatchMessageController {
+
+    @Resource
+    private ApiMessageFeignService apiMessageFeignService;
+
+    /**
+     * 开通成功通知家长
+     *
+     * @param request 请求
+     * @return
+     */
+    @PostMapping("/wristwatch/notifyParentsSuccessful/activation")
+    public CommonResult notifyParentsSuccessfulActivation(@Validated @RequestBody NotifyParentsSuccessfulActivationMessage request) {
+        return CommonResult.success(apiMessageFeignService.notifyParentsSuccessfulActivation(request));
+    }
+
+}

+ 25 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/websocket/vo/ReloginOnWwatchReqVO.java

@@ -0,0 +1,25 @@
+package cn.sikey.mcdisk.controller.app.websocket.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/3
+ * @Description: 手表重新登录
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ReloginOnWwatchReqVO {
+    /**
+     * 用户id
+     */
+    private String userId;
+
+    /**
+     * ticket
+     */
+    private String ticket;
+}

+ 19 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/wristwatch/AuthorizeController.java

@@ -1,7 +1,11 @@
 package cn.sikey.mcdisk.controller.app.wristwatch;
 
 import cn.sikey.framework.common.pojo.CommonResult;
+import cn.sikey.mcdisk.controller.app.websocket.vo.ReloginOnWwatchReqVO;
 import cn.sikey.mcdisk.controller.app.wristwatch.vo.*;
+import cn.sikey.mcdisk.entity.ReloginOnWwatchMessage;
+import cn.sikey.mcdisk.enums.CodeStatusEnum;
+import cn.sikey.mcdisk.enums.MessageType;
 import cn.sikey.mcdisk.enums.RateLimit;
 import cn.sikey.mcdisk.service.AuthorizeService;
 import cn.sikey.selenium.api.selenium.SmsApi;
@@ -13,6 +17,8 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 
+import java.time.Instant;
+
 /**
  * @Author: nelson
  * @Date: 2025/5/29
@@ -102,4 +108,17 @@ public class AuthorizeController {
     public CommonResult<AuthorizeRespVO> queryAuthorization(@Validated QueryOauthReqVO queryOauthReqVO) {
         return CommonResult.success(authorizeService.queryOauth(queryOauthReqVO.getTicket()));
     }
+
+    /**
+     * 手表退出登录
+     *
+     * @param logoutReqVO 手表退出登录
+     * @return
+     */
+    @PostMapping("/logout")
+    public CommonResult logout(@Validated @RequestBody LogoutReqVO logoutReqVO) {
+        // 更新设备授权状态
+        authorizeService.updateAuthorizeByDeviceId(logoutReqVO.getTicket(), CodeStatusEnum.LOG_OUT.getCode());
+        return CommonResult.success();
+    }
 }

+ 4 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/wristwatch/vo/AuthorizeRespVO.java

@@ -35,4 +35,8 @@ public class AuthorizeRespVO {
      * 授权accessToken过期时间
      */
     private LocalDateTime expireTime;
+    /**
+     * 状态:0未开通1已开通登录2已开通退出登录
+     */
+    private Integer authorizeStatus;
 }

+ 20 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/controller/app/wristwatch/vo/LogoutReqVO.java

@@ -0,0 +1,20 @@
+package cn.sikey.mcdisk.controller.app.wristwatch.vo;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: 手表退出登录
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class LogoutReqVO {
+    /**
+     * ticket
+     */
+    private String ticket;
+}

+ 5 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/dal/dataobject/AuthorizeDO.java

@@ -43,4 +43,9 @@ public class AuthorizeDO extends BaseDO {
      * 授权accessToken过期时间
      */
     private LocalDateTime expireTime;
+
+    /**
+     * 状态:0未开通1已开通登录2已开通退出登录
+     */
+    private Integer authorizeStatus;
 }

+ 2 - 6
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/dal/dataobject/AuthorizePhotoDO.java

@@ -29,16 +29,12 @@ public class AuthorizePhotoDO extends BaseDO {
      * 设备编码
      */
     private String deviceId;
-    /**
-     * 授权状态:1已授权2已创建
-     */
-    private Integer authorizeStatus;
     /**
      * 昵称
      */
     private String nickName;
     /**
-     * 设备名称
+     * 型号
      */
-    private String deviceName;
+    private String model;
 }

+ 10 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/dal/mysql/AuthorizeMapper.java

@@ -33,6 +33,7 @@ public interface AuthorizeMapper extends BaseMapper<AuthorizeDO> {
     default void updateAuthorizeByDeviceId(AuthorizeDO authorizeDO){
         UpdateWrapper<AuthorizeDO> updateWrapper = new UpdateWrapper<>();
         updateWrapper.eq("device_id", authorizeDO.getDeviceId());
+        updateWrapper.set("authorize_status",authorizeDO.getAuthorizeStatus());
         updateWrapper.set("cloud_id",authorizeDO.getCloudId());
         updateWrapper.set("photo_id",authorizeDO.getPhotoId());
         updateWrapper.set("update_time",authorizeDO.getUpdateTime());
@@ -50,6 +51,15 @@ public interface AuthorizeMapper extends BaseMapper<AuthorizeDO> {
         update(updateWrapper);
     }
 
+    default void updateAuthorizeStatusByDeviceId(AuthorizeDO authorizeDO){
+        UpdateWrapper<AuthorizeDO> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.eq("device_id", authorizeDO.getDeviceId());
+        updateWrapper.set("authorize_status",authorizeDO.getAuthorizeStatus());
+        updateWrapper.set("update_time",authorizeDO.getUpdateTime());
+        updateWrapper.set("updater",authorizeDO.getDeviceId());
+        update(updateWrapper);
+    }
+
 }
 
 

+ 3 - 5
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/dal/mysql/AuthorizePhotoMapper.java

@@ -27,9 +27,8 @@ public interface AuthorizePhotoMapper extends BaseMapper<AuthorizePhotoDO> {
         AuthorizePhotoDO authorizePhotoDO = new AuthorizePhotoDO();
         authorizePhotoDO.setUserId(userId);
         authorizePhotoDO.setDeviceId(ticket);
-        authorizePhotoDO.setAuthorizeStatus(IsAuthorizeEnum.AUTHORIZED.getValue());
         authorizePhotoDO.setNickName(appOauthPhotoReqVO.getNickName());
-        authorizePhotoDO.setDeviceName(appOauthPhotoReqVO.getDeviceName());
+        authorizePhotoDO.setModel(appOauthPhotoReqVO.getModel());
         authorizePhotoDO.setCreator(String.valueOf(userId));
         authorizePhotoDO.setUpdater(String.valueOf(userId));
         insert(authorizePhotoDO);
@@ -42,12 +41,11 @@ public interface AuthorizePhotoMapper extends BaseMapper<AuthorizePhotoDO> {
         return selectOne(queryWrapper);
     }*/
 
-    default void updateAuthorizePhotoByUserIdDeviceId(String deviceId){
+    /*default void updateAuthorizePhotoByUserIdDeviceId(String deviceId){
         UpdateWrapper<AuthorizePhotoDO> queryWrapper = new UpdateWrapper<>();
         queryWrapper.eq("device_id", deviceId);
-        queryWrapper.set("is_authorize",IsAuthorizeEnum.CREATED.getValue());
         update(queryWrapper);
-    }
+    }*/
 
     default AuthorizePhotoDO queryOauthPhotoByDeviceId(String deviceId){
         QueryWrapper<AuthorizePhotoDO> queryWrapper = new QueryWrapper<>();

+ 24 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/ActivateCloudPhotoAlbumMessage.java

@@ -0,0 +1,24 @@
+package cn.sikey.mcdisk.entity;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/3
+ * @Description: 开通云相册
+ */
+@Data
+@Accessors(chain = true)
+public class ActivateCloudPhotoAlbumMessage extends BasicMessage{
+    /**
+     * 消息内容
+     */
+    private Content content;
+
+    @Data
+    @Accessors(chain = true)
+    public static class Content {
+        private String ticket;
+    }
+}

+ 49 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/BasicMessage.java

@@ -0,0 +1,49 @@
+package cn.sikey.mcdisk.entity;
+
+import cn.sikey.mcdisk.enums.MessageType;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+
+/**
+ * 基础消息
+ */
+@Data
+@Accessors(chain = true)
+@ToString(callSuper = true)
+public class BasicMessage {
+
+    /**
+     * 消息类型
+     *
+     */
+    private MessageType msgType;
+
+    /**
+     * 应答ID <br/>
+     * Websocket 服务在将消息发送之前,需要查询 message_recv 表将数据带上
+     */
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private Long ackId;
+
+    /**
+     * 消息ID
+     */
+    private Long msgId;
+
+    /**
+     * 接收人ID
+     */
+    private String recvId;
+
+    /**
+     * 发送人ID
+     */
+    private String sendId;
+
+    /**
+     * 发送时间
+     */
+    private Long sendTime;
+}

+ 0 - 39
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/FamilyCloudList.java

@@ -1,39 +0,0 @@
-package cn.sikey.mcdisk.entity;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-
-import java.util.List;
-
-/**
- * @Author: nelson
- * @Date: 2025/5/29
- * @Description: 用户家庭云列表
- */
-@Data
-@AllArgsConstructor
-public class FamilyCloudList {
-    private List<CloudInfo> familyCloudList;
-}
-
-@Data
-@AllArgsConstructor
-class CloudInfo {
-    private CommonAccountInfo commonAccountInfo;
-    private String nickname;
-    private String cloudNickName;
-    private String cloudID;
-    private String cloudName;
-    private String cloudDesc;
-    private int cloudType;
-    private String createTime;
-    private String lastUpdateTime;
-}
-
-@Data
-@AllArgsConstructor
-class CommonAccountInfo {
-    private String account;
-    private String accountUserId;
-    private String accountType;
-}

+ 15 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/MessagingPublishResponse.java

@@ -0,0 +1,15 @@
+package cn.sikey.mcdisk.entity;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+@Data
+@Accessors(chain = true)
+public class MessagingPublishResponse implements Serializable {
+    /**
+     * 消息ID
+     */
+    private Long msgId;
+}

+ 27 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/NotifyParentsSuccessfulActivationMessage.java

@@ -0,0 +1,27 @@
+package cn.sikey.mcdisk.entity;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/3
+ * @Description: 开通成功通知家长
+ */
+@Data
+@Accessors(chain = true)
+public class NotifyParentsSuccessfulActivationMessage extends BasicMessage {
+    /**
+     * 消息内容
+     */
+    private Content content;
+
+    @Data
+    @Accessors(chain = true)
+    public static class Content {
+        /**
+         * 用户id
+         */
+        private String userId;
+    }
+}

+ 28 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/entity/ReloginOnWwatchMessage.java

@@ -0,0 +1,28 @@
+package cn.sikey.mcdisk.entity;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/3
+ * @Description: 手表端重新登录
+ */
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+public class ReloginOnWwatchMessage extends BasicMessage{
+    /**
+     * 消息内容
+     */
+    private Content content;
+
+    @Data
+    @Accessors(chain = true)
+    public static class Content {
+        /**
+         * 用户id
+         */
+        private String userId;
+    }
+}

+ 7 - 3
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/enums/CodeStatusEnum.java

@@ -19,11 +19,15 @@ public enum CodeStatusEnum implements IntArrayValuable {
 
     USER_HAS_BEEN_AUTHENTICATED(1003, "用户已认证授权"),
 
-    WATCH_HAS_NOT_YET_ENABLED_CLOUD_PHOTO_ALBUM_SERVICE(2001, "手表暂未开通云相册服务"),
+    PHOTO_HAS_BEEN_UNAUTHORIZED(0, "未授权"),
 
-    PHOTO_HAS_BEEN_AUTHENTICATED(2002, "相册已授权"),
+    PHOTO_HAS_BEEN_AUTHENTICATED(1, "已授权"),
 
-    PHOTO_HAS_BEEN_CREATED(2003, "相册已创建")
+    NOT_ACTIVATED(0, "未开通"),
+
+    ALREADY_ACTIVATED(1, "已开通"),
+
+    LOG_OUT(2, "退出登录"),
     ;
 
 

+ 1 - 1
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/enums/IsAuthorizeEnum.java

@@ -17,7 +17,7 @@ import java.util.Arrays;
 public enum IsAuthorizeEnum implements IntArrayValuable {
     UNAUTHORIZED(0, "未授权"),
     AUTHORIZED(1, "已授权"),
-    CREATED(2, "已创建"),
+    AUTHORIZED_EXIST(1, "已开通退出登录")
     ;
 
     public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IsAuthorizeEnum::getValue).toArray();

+ 53 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/enums/MessageType.java

@@ -0,0 +1,53 @@
+package cn.sikey.mcdisk.enums;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 消息类型
+ */
+@AllArgsConstructor
+@Getter
+@JsonFormat(shape = JsonFormat.Shape.OBJECT)
+public enum MessageType {
+
+    /**
+     * 开通成功通知家长
+     */
+    NOTIFY_PARENTS_SUCCESSFUL_ACTIVATION(62, "publish.notice.notify-parents-successful-activation"),
+
+    /**
+     * 手表重新登录
+     */
+    RELOGIN_ON_WWATCH(63, "publish.notice.relogin-on-watch"),
+
+    /**
+     * 开通云相册服务通知手表
+     */
+    ACTIVATE_CLOUD_PHOTO_ALBUM_SERVICE(100, "publish.notice.activate-cloud-photo-album-service"),
+
+    ;
+
+    @JsonValue
+    private final Integer value;
+    private final String routingKey;
+
+    /**
+     * 根据 value 获取 MessageType
+     *
+     * @param value value
+     * @return MessageType
+     */
+    public static MessageType valueOf(Integer value) {
+        for (MessageType messageType : MessageType.values()) {
+            if (messageType.value.equals(value)) {
+                return messageType;
+            }
+        }
+        throw new RuntimeException("Invalid MessageType value: " + value);
+    }
+}

+ 47 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/feign/ApiMessageFeignService.java

@@ -0,0 +1,47 @@
+package cn.sikey.mcdisk.feign;
+
+import cn.sikey.mcdisk.entity.ActivateCloudPhotoAlbumMessage;
+import cn.sikey.mcdisk.entity.MessagingPublishResponse;
+import cn.sikey.mcdisk.entity.NotifyParentsSuccessfulActivationMessage;
+import cn.sikey.mcdisk.entity.ReloginOnWwatchMessage;
+import cn.sikey.mcdisk.feign.fallback.ApiMessageFallbackFactory;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 消息中心feign
+ */
+@FeignClient(value = "api-message", path = "/api/v2/messagectx/messaging/publish", fallbackFactory = ApiMessageFallbackFactory.class)
+public interface ApiMessageFeignService {
+
+    /**
+     * 手表重新登录
+     *
+     * @param request 请求
+     * @return
+     */
+    @PostMapping("/reloginWatch")
+    MessagingPublishResponse reloginOnWwatch(@Validated @RequestBody ReloginOnWwatchMessage request);
+
+    /**
+     * 开通云相册服务通知手表
+     *
+     * @param request 请求
+     * @return
+     */
+    @PostMapping("/activateCloudPhotoAlbum")
+    MessagingPublishResponse activateCloudPhotoAlbum(@Validated @RequestBody ActivateCloudPhotoAlbumMessage request);
+
+    /**
+     * 开通成功通知家长
+     *
+     * @param request 请求
+     * @return
+     */
+    @PostMapping("/wristwatch/notifyParentsSuccessful/activation")
+    MessagingPublishResponse notifyParentsSuccessfulActivation(@Validated @RequestBody NotifyParentsSuccessfulActivationMessage request);
+}

+ 52 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/feign/fallback/ApiMessageFallbackFactory.java

@@ -0,0 +1,52 @@
+package cn.sikey.mcdisk.feign.fallback;
+
+import cn.hutool.http.HttpStatus;
+import cn.sikey.framework.common.exception.ServiceException;
+import cn.sikey.mcdisk.entity.ActivateCloudPhotoAlbumMessage;
+import cn.sikey.mcdisk.entity.MessagingPublishResponse;
+import cn.sikey.mcdisk.entity.NotifyParentsSuccessfulActivationMessage;
+import cn.sikey.mcdisk.entity.ReloginOnWwatchMessage;
+import cn.sikey.mcdisk.feign.ApiMessageFeignService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.openfeign.FallbackFactory;
+import org.springframework.stereotype.Component;
+
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 消息中心feign降级服务
+ */
+@Slf4j
+@Component
+public class ApiMessageFallbackFactory implements FallbackFactory<ApiMessageFeignService> {
+
+    /**
+     * 获取用户登录信息
+     *
+     * @param cause 用户登录信息
+     * @return
+     */
+    @Override
+    public ApiMessageFeignService create(Throwable cause) {
+        return new ApiMessageFeignService() {
+            @Override
+            public MessagingPublishResponse reloginOnWwatch(ReloginOnWwatchMessage request) {
+                log.error("[消息中心feign降级服务]调用手表重新登录发消息失败:{}", cause);
+                throw new ServiceException(HttpStatus.HTTP_INTERNAL_ERROR, "调用手表重新登录发消息失败");
+            }
+
+            @Override
+            public MessagingPublishResponse activateCloudPhotoAlbum(ActivateCloudPhotoAlbumMessage request) {
+                log.error("[消息中心feign降级服务]调用开通云相册服务通知手表失败:{}", cause);
+                throw new ServiceException(HttpStatus.HTTP_INTERNAL_ERROR, "调用开通云相册服务通知手表失败");
+            }
+
+            @Override
+            public MessagingPublishResponse notifyParentsSuccessfulActivation(NotifyParentsSuccessfulActivationMessage request) {
+                log.error("[消息中心feign降级服务]调用开通成功通知家长失败:{}", cause);
+                throw new ServiceException(HttpStatus.HTTP_INTERNAL_ERROR, "调用开通成功通知家长失败");
+            }
+        };
+    }
+}

+ 2 - 5
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/AuthorizePhotoService.java

@@ -1,10 +1,7 @@
 package cn.sikey.mcdisk.service;
 
 import cn.sikey.framework.common.pojo.CommonResult;
-import cn.sikey.mcdisk.controller.app.authorizephoto.vo.AppOauthPhotoReqVO;
-import cn.sikey.mcdisk.controller.app.authorizephoto.vo.AuthorizePhotoRespVO;
-import cn.sikey.mcdisk.controller.app.authorizephoto.vo.OauthPhotoReqVO;
-import cn.sikey.mcdisk.controller.app.authorizephoto.vo.QueryOauthPhotoByDeviceIdRespVO;
+import cn.sikey.mcdisk.controller.app.authorizephoto.vo.*;
 
 /**
  * @Author: nelson
@@ -19,6 +16,6 @@ public interface AuthorizePhotoService {
 
     QueryOauthPhotoByDeviceIdRespVO queryOauthPhotoByDeviceId(String deviceId);
 
-    void updateAuthorizePhotoByUserIdDeviceId(String deviceId);
+    void notifyWatchAuthorization(NotifyWatchAuthorizationReqVO notifyWatchAuthorizationReqVO);
 
 }

+ 48 - 30
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/AuthorizePhotoServiceImpl.java

@@ -2,20 +2,23 @@ package cn.sikey.mcdisk.service;
 
 import cn.sikey.framework.common.exception.ServiceException;
 import cn.sikey.framework.common.pojo.CommonResult;
-import cn.sikey.mcdisk.controller.app.authorizephoto.vo.AppOauthPhotoReqVO;
-import cn.sikey.mcdisk.controller.app.authorizephoto.vo.AuthorizePhotoRespVO;
-import cn.sikey.mcdisk.controller.app.authorizephoto.vo.OauthPhotoReqVO;
-import cn.sikey.mcdisk.controller.app.authorizephoto.vo.QueryOauthPhotoByDeviceIdRespVO;
-import cn.sikey.mcdisk.controller.app.wristwatch.vo.AuthorizeRespVO;
+import cn.sikey.mcdisk.controller.app.authorizephoto.vo.*;
 import cn.sikey.mcdisk.convert.AuthorizePhotoConvert;
 import cn.sikey.mcdisk.dal.dataobject.AuthorizePhotoDO;
 import cn.sikey.mcdisk.dal.mysql.AuthorizePhotoMapper;
+import cn.sikey.mcdisk.entity.ActivateCloudPhotoAlbumMessage;
 import cn.sikey.mcdisk.enums.CodeStatusEnum;
 import cn.sikey.mcdisk.enums.IsAuthorizeEnum;
+import cn.sikey.mcdisk.enums.MessageType;
+import cn.sikey.mcdisk.feign.ApiMessageFeignService;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 
+import java.time.Instant;
 import java.util.Objects;
 
 /**
@@ -33,6 +36,16 @@ public class AuthorizePhotoServiceImpl implements AuthorizePhotoService {
     @Resource
     private AuthorizeService authorizeService;
 
+    @Resource
+    private ApiMessageFeignService apiMessageFeignService;
+
+    @Resource
+    private MessageIdIncrementingService<Long> messageIdIncrementingService;
+
+    @Qualifier("myTaskExecutor")
+    @Autowired
+    private ThreadPoolTaskExecutor myTaskExecutor;
+
     /**
      * 授权相册
      *
@@ -47,26 +60,10 @@ public class AuthorizePhotoServiceImpl implements AuthorizePhotoService {
 
         AuthorizePhotoDO authorizePhotoD = authorizePhotoMapper.queryOauthPhotoByDeviceId(ticket);
         if (Objects.nonNull(authorizePhotoD)) {
-            log.warn("[授权相册]存在相册授权数据: {}", authorizePhotoD);
-
-            if (authorizePhotoD.getAuthorizeStatus().intValue() == IsAuthorizeEnum.AUTHORIZED.getValue()) {
-                throw new ServiceException(CodeStatusEnum.PHOTO_HAS_BEEN_AUTHENTICATED.getCode(), CodeStatusEnum.PHOTO_HAS_BEEN_AUTHENTICATED.getDescription());
-            } else if (authorizePhotoD.getAuthorizeStatus().intValue() == IsAuthorizeEnum.CREATED.getValue()) {
-                throw new ServiceException(CodeStatusEnum.PHOTO_HAS_BEEN_CREATED.getCode(), CodeStatusEnum.PHOTO_HAS_BEEN_CREATED.getDescription());
-            }
-
+            throw new ServiceException(CodeStatusEnum.PHOTO_HAS_BEEN_AUTHENTICATED.getCode(), CodeStatusEnum.PHOTO_HAS_BEEN_AUTHENTICATED.getDescription());
         }
 
-        // 设备授权
-        AuthorizeRespVO authorizeRespVO = authorizeService.queryAuthorize(ticket);
-        if (Objects.isNull(authorizeRespVO)) {
-            // 相册授权
-            authorizePhotoMapper.insertOauthPhoto(appOauthPhotoReqVO);
-            throw new ServiceException(CodeStatusEnum.WATCH_HAS_NOT_YET_ENABLED_CLOUD_PHOTO_ALBUM_SERVICE.getCode(), CodeStatusEnum.WATCH_HAS_NOT_YET_ENABLED_CLOUD_PHOTO_ALBUM_SERVICE.getDescription());
-        }
-
-        // 更新相册授权状态
-        authorizePhotoMapper.updateAuthorizePhotoByUserIdDeviceId(ticket);
+        authorizePhotoMapper.insertOauthPhoto(appOauthPhotoReqVO);
 
         log.info("[授权相册]授权成功");
 
@@ -81,28 +78,49 @@ public class AuthorizePhotoServiceImpl implements AuthorizePhotoService {
      */
     @Override
     public QueryOauthPhotoByDeviceIdRespVO queryOauthPhotoByDeviceId(String deviceId) {
-        QueryOauthPhotoByDeviceIdRespVO queryOauthPhotoByDeviceIdRespVO = null;
+        QueryOauthPhotoByDeviceIdRespVO queryOauthPhotoByDeviceIdRespVO = new QueryOauthPhotoByDeviceIdRespVO();
 
         AuthorizePhotoDO authorizePhotoDO = authorizePhotoMapper.queryOauthPhotoByDeviceId(deviceId);
         if (Objects.nonNull(authorizePhotoDO)) {
-            queryOauthPhotoByDeviceIdRespVO = new QueryOauthPhotoByDeviceIdRespVO();
             queryOauthPhotoByDeviceIdRespVO.setId(authorizePhotoDO.getId());
-            queryOauthPhotoByDeviceIdRespVO.setDeviceName(authorizePhotoDO.getDeviceName());
+            queryOauthPhotoByDeviceIdRespVO.setUserId(authorizePhotoDO.getUserId());
+            queryOauthPhotoByDeviceIdRespVO.setDeviceName(authorizePhotoDO.getModel());
             queryOauthPhotoByDeviceIdRespVO.setNickName(authorizePhotoDO.getNickName());
+            queryOauthPhotoByDeviceIdRespVO.setAuthorizeStatus(IsAuthorizeEnum.AUTHORIZED.getValue());
+        } else {
+            queryOauthPhotoByDeviceIdRespVO.setAuthorizeStatus(IsAuthorizeEnum.UNAUTHORIZED.getValue());
         }
 
+        log.info("[查询授权相册]返回数据:{}", queryOauthPhotoByDeviceIdRespVO);
         return queryOauthPhotoByDeviceIdRespVO;
     }
 
     /**
-     * 更新相册授权
+     * 通知手表授权
      *
-     * @param deviceId 设备编码
+     * @param notifyWatchAuthorizationReqVO 通知手表授权
      * @return
      */
     @Override
-    public void updateAuthorizePhotoByUserIdDeviceId(String deviceId) {
-        authorizePhotoMapper.updateAuthorizePhotoByUserIdDeviceId(deviceId);
+    public void notifyWatchAuthorization(NotifyWatchAuthorizationReqVO notifyWatchAuthorizationReqVO) {
+        // 设备授权
+        myTaskExecutor.execute(() -> {
+            // 推送通知设备
+            ActivateCloudPhotoAlbumMessage request = new ActivateCloudPhotoAlbumMessage();
+            request.setMsgId(messageIdIncrementingService.incr());
+            request.setMsgType(MessageType.ACTIVATE_CLOUD_PHOTO_ALBUM_SERVICE);
+            request.setSendId(notifyWatchAuthorizationReqVO.getUserId());
+            request.setRecvId(notifyWatchAuthorizationReqVO.getTicket());
+            request.setSendTime(Instant.now().toEpochMilli());
+            ActivateCloudPhotoAlbumMessage.Content content = new ActivateCloudPhotoAlbumMessage.Content();
+            content.setTicket(notifyWatchAuthorizationReqVO.getTicket());
+            request.setContent(content);
+            apiMessageFeignService.activateCloudPhotoAlbum(request);
+            log.info("[通知手表授权]发送推送成功:{}", request);
+        });
+
+        throw new ServiceException(CodeStatusEnum.NOT_ACTIVATED.getCode(), CodeStatusEnum.NOT_ACTIVATED.getDescription());
+
     }
 
     /**

+ 2 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/AuthorizeService.java

@@ -20,4 +20,6 @@ public interface AuthorizeService {
 
     AuthorizeRespVO queryAuthorize(String deviceId);
 
+    void updateAuthorizeByDeviceId(String deviceId,Integer authorizeStatus);
+
 }

+ 47 - 6
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/AuthorizeServiceImpl.java

@@ -13,14 +13,15 @@ import cn.sikey.mcdisk.controller.app.wristwatch.vo.RefreshTokenReqVO;
 import cn.sikey.mcdisk.convert.OauthConvert;
 import cn.sikey.mcdisk.dal.dataobject.AuthorizeDO;
 import cn.sikey.mcdisk.dal.mysql.AuthorizeMapper;
-import cn.sikey.mcdisk.enums.AccountTypeEnum;
-import cn.sikey.mcdisk.enums.ApiEndpointsEnum;
-import cn.sikey.mcdisk.enums.CodeStatusEnum;
+import cn.sikey.mcdisk.entity.NotifyParentsSuccessfulActivationMessage;
+import cn.sikey.mcdisk.enums.*;
+import cn.sikey.mcdisk.feign.ApiMessageFeignService;
 import cn.sikey.mcdisk.pojo.McdiskResult;
 import jakarta.annotation.Resource;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
+import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.temporal.ChronoUnit;
 import java.util.Objects;
@@ -48,6 +49,12 @@ public class AuthorizeServiceImpl extends AbstractMcdiskService implements Autho
 
     private static final String MY_FAMILY_CLOUD = "我的家庭云";
 
+    @Resource
+    private MessageIdIncrementingService<Long> messageIdIncrementingService;
+
+    @Resource
+    private ApiMessageFeignService apiMessageFeignService;
+
     /**
      * 设备授权
      *
@@ -81,8 +88,27 @@ public class AuthorizeServiceImpl extends AbstractMcdiskService implements Autho
             authorizeDO.setCloudId(cloudID);
             authorizeDO.setPhotoId(photoID);
             authorizeDO.setUpdater(ticket);
+            authorizeDO.setAuthorizeStatus(IsAuthorizeEnum.AUTHORIZED_EXIST.getValue());
             authorizeDO.setUpdateTime(LocalDateTime.now());
             authorizeMapper.updateAuthorizeByDeviceId(authorizeDO);
+
+            QueryOauthPhotoByDeviceIdRespVO queryOauthPhotoByDeviceIdRespVO = authorizePhotoService.queryOauthPhotoByDeviceId(authorizeDO.getDeviceId());
+            String userId = "";
+            if (Objects.nonNull(queryOauthPhotoByDeviceIdRespVO)) {
+                userId = queryOauthPhotoByDeviceIdRespVO.getUserId();
+            }
+            // 推送
+            NotifyParentsSuccessfulActivationMessage request = new NotifyParentsSuccessfulActivationMessage();
+            request.setMsgId(messageIdIncrementingService.incr());
+            request.setMsgType(MessageType.NOTIFY_PARENTS_SUCCESSFUL_ACTIVATION);
+            request.setSendId(userId);
+            request.setRecvId(ticket);
+            request.setSendTime(Instant.now().toEpochMilli());
+            NotifyParentsSuccessfulActivationMessage.Content content = new NotifyParentsSuccessfulActivationMessage.Content();
+            content.setUserId(userId);
+            request.setContent(content);
+            apiMessageFeignService.notifyParentsSuccessfulActivation(request);
+
             log.info("[授权]更新设备授权数据成功");
         });
 
@@ -154,10 +180,8 @@ public class AuthorizeServiceImpl extends AbstractMcdiskService implements Autho
             QueryOauthPhotoByDeviceIdRespVO queryOauthPhotoByDeviceIdRespVO = authorizePhotoService.queryOauthPhotoByDeviceId(deviceId);
             String photoName = "";
             if (Objects.nonNull(queryOauthPhotoByDeviceIdRespVO)) {
-                // 授权相册
-                authorizePhotoService.updateAuthorizePhotoByUserIdDeviceId(deviceId);
-
                 photoName = queryOauthPhotoByDeviceIdRespVO.getNickName() + "-" + queryOauthPhotoByDeviceIdRespVO.getDeviceName();
+
             }
 
             // 手表授权获取云id,相册id
@@ -225,4 +249,21 @@ public class AuthorizeServiceImpl extends AbstractMcdiskService implements Autho
         AuthorizeRespVO authorizeRespVO = OauthConvert.INSTANCE.convertOauthRespVO(authorizeDO);
         return authorizeRespVO;
     }
+
+    /**
+     * 更新设备授权状态
+     *
+     * @param deviceId        设备id
+     * @param authorizeStatus 手表状态
+     * @return
+     */
+    @Override
+    public void updateAuthorizeByDeviceId(String deviceId, Integer authorizeStatus) {
+        AuthorizeDO authorizeDO = new AuthorizeDO();
+        authorizeDO.setDeviceId(deviceId);
+        authorizeDO.setAuthorizeStatus(authorizeStatus);
+        authorizeDO.setUpdater(deviceId);
+        authorizeDO.setUpdateTime(LocalDateTime.now());
+        authorizeMapper.updateAuthorizeStatusByDeviceId(authorizeDO);
+    }
 }

+ 23 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/MessageIdIncrementingService.java

@@ -0,0 +1,23 @@
+package cn.sikey.mcdisk.service;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 消息id递增适配器
+ */
+public interface MessageIdIncrementingService<ID> {
+
+    /**
+     * 递增
+     *
+     * @return 递增后的ID
+     */
+    ID incr();
+
+    /**
+     * 递增
+     *
+     * @return 递增后的ID
+     */
+    ID incr(String key);
+}

+ 39 - 0
sikey-mcdisk-business/sikey-mcdisk-business-biz/src/main/java/cn/sikey/mcdisk/service/MessageIdIncrementingServiceImpl.java

@@ -0,0 +1,39 @@
+package cn.sikey.mcdisk.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisOperations;
+import org.springframework.stereotype.Component;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 使用 Redis 构建的消息ID递增适配器
+ */
+@Slf4j
+@Component
+public class MessageIdIncrementingServiceImpl implements MessageIdIncrementingService<Long> {
+    /**
+     * Redis incr 的 key <br/>
+     * 消息ID通过 Redis 生成
+     */
+    private static final String MESSAGE_ID_INCREMENTING_KEY = "message.mcdisk.id";
+
+    private final RedisOperations<String, String> redisOperations;
+
+    @Autowired
+    public MessageIdIncrementingServiceImpl(RedisOperations<String, String> redisOperations) {
+        this.redisOperations = redisOperations;
+    }
+
+    @Override
+    public Long incr() {
+        return redisOperations.opsForValue().increment(MESSAGE_ID_INCREMENTING_KEY);
+    }
+
+    @Override
+    public Long incr(String key) {
+        return redisOperations.opsForValue().increment(key);
+    }
+
+}

+ 2 - 2
sikey-selenium-business/sikey-selenium-business-biz/src/main/java/cn/sikey/selenium/api/SmsServiceImpl.java

@@ -62,7 +62,7 @@ public class SmsServiceImpl implements SmsApi {
 
     public static final int GET_VERIFICATION_CODE_AGAIN = 10001;
 
-    public static final int UUID_EXPIRES_MINUTE = 15;
+    public static final int UUID_EXPIRES_MINUTE = 2;
 
     public static final String SMS_CODE_KEY = "sms:code:phone:";
 
@@ -219,7 +219,7 @@ public class SmsServiceImpl implements SmsApi {
                 log.info("[校验验证码登录]登录成功,手机号:{},验证码:{}", phoneNumber, verificationCode);
 
                 // 家庭云id,相册id
-                
+
             } catch (Exception e) {
                 log.error("[校验验证码登录]异常:{0}", e);
                 throw new ServiceException(HttpStatus.SC_INTERNAL_SERVER_ERROR, "触发校验验证码登录失败");

+ 22 - 0
sikey-selenium-business/sikey-selenium-business-biz/src/main/java/cn/sikey/selenium/scheduler/KeepItAliveScheduler.java

@@ -0,0 +1,22 @@
+package cn.sikey.selenium.scheduler;
+
+import cn.sikey.selenium.util.WebDriverContextManagerUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class KeepItAliveScheduler {
+
+
+    /**
+     * 检查Session上次活跃时间 秒检查一次
+     */
+    @Scheduled(cron = "*/30 * * * * *")
+    public void run() {
+        WebDriverContextManagerUtil.sessionMap.asMap().forEach((k, v) -> {
+            WebDriverContextManagerUtil.destroySession(k);
+        });
+    }
+}

+ 10 - 3
sikey-selenium-business/sikey-selenium-business-biz/src/main/java/cn/sikey/selenium/util/WebDriverContextManagerUtil.java

@@ -2,6 +2,8 @@ package cn.sikey.selenium.util;
 
 import cn.hutool.http.HttpStatus;
 import cn.sikey.framework.common.exception.ServiceException;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.chrome.ChromeOptions;
 import org.openqa.selenium.remote.RemoteWebDriver;
@@ -12,6 +14,7 @@ import java.net.URL;
 import java.time.Duration;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
 
 
 /**
@@ -21,7 +24,11 @@ import java.util.concurrent.ConcurrentHashMap;
  */
 public class WebDriverContextManagerUtil {
 
-    private static final Map<String, DriverSession> sessionMap = new ConcurrentHashMap<>();
+    public final static Cache<String, DriverSession> sessionMap = CacheBuilder.newBuilder()
+            .maximumSize(10000L)
+            .expireAfterWrite(30, TimeUnit.MINUTES)
+            .build();
+
 
     private static final long DEFAULT_TIMEOUT = 30;
 
@@ -43,7 +50,7 @@ public class WebDriverContextManagerUtil {
 
     // 获取现有会话
     public static DriverSession getSession(String sessionId) {
-        DriverSession session = sessionMap.get(sessionId);
+        DriverSession session = sessionMap.getIfPresent(sessionId);
         if (session == null) {
             throw new ServiceException(HttpStatus.HTTP_INTERNAL_ERROR, "无效的会话ID");
         }
@@ -60,7 +67,7 @@ public class WebDriverContextManagerUtil {
 
     // 销毁会话
     public static void destroySession(String sessionId) {
-        DriverSession session = sessionMap.remove(sessionId);
+        DriverSession session = sessionMap.getIfPresent(sessionId);
         if (session != null) {
             session.getDriver().quit();
         }

+ 292 - 0
sikey-websocket-business/pom.xml

@@ -0,0 +1,292 @@
+<?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>cn.sikey.cloud</groupId>
+        <artifactId>sikey-business</artifactId>
+        <version>2.3.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>sikey-websocket-business</artifactId>
+    <packaging>pom</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>API selenium,基于 Spring Cloud selenium 实现</description>
+
+    <modules>
+        <module>sikey-websocket-business-api</module>
+        <module>sikey-websocket-business-biz</module>
+    </modules>
+
+    <properties>
+        <java.version>23</java.version>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
+        <consul.version>4.2.0</consul.version>
+        <spring.cloud.gateway.version>4.2.0</spring.cloud.gateway.version>
+        <!--<spring.cloud.alibaba.version>2023.0.1.0</spring.cloud.alibaba.version>-->
+        <lombok.version>1.18.36</lombok.version>
+        <logback.version>1.2.12</logback.version>
+        <mapstruct.version>1.6.2</mapstruct.version>
+        <spring.cloud.version>2024.0.0</spring.cloud.version>
+        <spring.boot.version>3.4.0</spring.boot.version>
+        <skywalking.version>9.0.0</skywalking.version>
+        <opentracing.version>0.33.0</opentracing.version>
+        <revision>2.3.0-SNAPSHOT</revision>
+
+        <maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>
+        <maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
+        <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
+        <okhttp3.version>4.12.0</okhttp3.version>
+        <hutool.version>5.8.28</hutool.version>
+        <google.zxing.version>3.5.2</google.zxing.version>
+        <alipay.v3.version>3.0.0.ALL</alipay.v3.version>
+        <wechat.v3.version>0.2.16</wechat.v3.version>
+        <gson.version>2.11.0</gson.version>
+        <security.version>3.4.4</security.version>
+        <commons-codec.version>1.15</commons-codec.version>
+
+        <spring.boot.starter.version>3.4.0</spring.boot.starter.version>
+    </properties>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-dependencies</artifactId>
+                <version>${spring.cloud.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-dependencies</artifactId>
+                <version>${spring.boot.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
+            <!-- Web 开发基础依赖 -->
+            <dependency>
+                <groupId>cn.sikey.cloud</groupId>
+                <artifactId>sikey-spring-boot-starter-web</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- Registry 注册中心相关 -->
+            <!--<dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+            </dependency>-->
+
+            <!-- Config 配置中心相关 -->
+            <!--<dependency>
+                <groupId>com.alibaba.cloud</groupId>
+                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+            </dependency>-->
+
+            <!-- Spring Cloud Consul 配置中心 -->
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-starter-consul-config</artifactId>
+                <version>${consul.version}</version>
+            </dependency>
+
+            <!-- Spring Cloud Consul 服务发现 -->
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-starter-consul-discovery</artifactId>
+                <version>${consul.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.sikey.cloud</groupId>
+                <artifactId>sikey-common</artifactId>
+                <version>2.3.0-SNAPSHOT</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.projectlombok</groupId>
+                <artifactId>lombok</artifactId>
+                <version>${lombok.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct</artifactId>
+                <version>${mapstruct.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct-jdk8</artifactId>
+                <version>${mapstruct.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.mapstruct</groupId>
+                <artifactId>mapstruct-processor</artifactId>
+                <version>${mapstruct.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.apache.skywalking</groupId>
+                <artifactId>apm-toolkit-trace</artifactId>
+                <version>${skywalking.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.skywalking</groupId>
+                <artifactId>apm-toolkit-logback-1.x</artifactId>
+                <version>${skywalking.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.skywalking</groupId>
+                <artifactId>apm-toolkit-opentracing</artifactId>
+                <version>${skywalking.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>io.opentracing</groupId>
+                <artifactId>opentracing-api</artifactId>
+                <version>${opentracing.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.opentracing</groupId>
+                <artifactId>opentracing-util</artifactId>
+                <version>${opentracing.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.opentracing</groupId>
+                <artifactId>opentracing-noop</artifactId>
+                <version>${opentracing.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>com.squareup.okhttp3</groupId>
+                <artifactId>okhttp</artifactId>
+                <version>${okhttp3.version}</version>
+            </dependency>
+
+            <!-- Hutool 核心库 -->
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-all</artifactId>
+                <version>${hutool.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>cn.hutool</groupId>
+                <artifactId>hutool-core</artifactId>
+                <version>${hutool.version}</version>
+            </dependency>
+
+            <!-- 显式添加 Gson -->
+            <dependency>
+                <groupId>com.google.code.gson</groupId>
+                <artifactId>gson</artifactId>
+                <version>${gson.version}</version>
+            </dependency>
+
+            <!--<dependency>
+                <groupId>org.apache.tomcat.embed</groupId>
+                <artifactId>tomcat-embed-core</artifactId>
+            </dependency>-->
+            <dependency>
+                <groupId>commons-codec</groupId>
+                <artifactId>commons-codec</artifactId>
+                <version>${commons-codec.version}</version>
+            </dependency>
+
+            <dependency>
+                <groupId>ch.qos.logback</groupId>
+                <artifactId>logback-core</artifactId>
+                <version>1.5.12</version>
+            </dependency>
+            <dependency>
+                <groupId>ch.qos.logback</groupId>
+                <artifactId>logback-classic</artifactId>
+                <version>1.5.12</version>
+            </dependency>
+
+            <dependency>
+                <groupId>org.slf4j</groupId>
+                <artifactId>slf4j-api</artifactId>
+                <version>2.0.16</version>
+            </dependency>
+
+            <!-- 参数校验 -->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-validation</artifactId>
+                <version>${spring.boot.version}</version>
+            </dependency>
+
+            <!-- DB 相关 -->
+            <dependency>
+                <groupId>cn.sikey.cloud</groupId>
+                <artifactId>sikey-spring-boot-starter-mybatis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+            <!-- Redis 相关 -->
+            <dependency>
+                <groupId>cn.sikey.cloud</groupId>
+                <artifactId>sikey-spring-boot-starter-redis</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
+
+            <!-- Spring Boot AMQP Starter -->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-amqp</artifactId>
+                <version>${spring.boot.starter.version}</version>
+            </dependency>
+
+            <!-- Spring Boot WebSocket Starter -->
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-websocket</artifactId>
+                <version>${spring.boot.starter.version}</version>
+            </dependency>
+
+
+            <!--<dependency>
+                <groupId>com.microsoft.playwright</groupId>
+                <artifactId>playwright</artifactId>
+                <version>${playwright.version}</version>
+            </dependency>-->
+
+            <!-- RPC 远程调用相关 -->
+            <!--<dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+                <version>4.1.1</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.cloud</groupId>
+                <artifactId>spring-cloud-starter-openfeign</artifactId>
+                <version>4.1.1</version>
+            </dependency>
+            <dependency>
+                <groupId>io.github.openfeign</groupId>
+                <artifactId>feign-okhttp</artifactId>
+                <version>12.5</version>
+            </dependency>-->
+
+            <!--<dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-data-jpa</artifactId>
+                <version>3.4.4</version>
+            </dependency>-->
+
+
+            <!--<dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-security</artifactId>
+                <version>${security.version}</version>
+            </dependency>-->
+        </dependencies>
+    </dependencyManagement>
+
+</project>

+ 128 - 0
sikey-websocket-business/sikey-websocket-business-api/pom.xml

@@ -0,0 +1,128 @@
+<?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>cn.sikey.cloud</groupId>
+        <artifactId>sikey-websocket-business</artifactId>
+        <version>2.3.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>sikey-websocket-business-api</artifactId>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        websocket 模块 API,暴露给其它模块调用
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.sikey.cloud</groupId>
+            <artifactId>sikey-common</artifactId>
+            <version>2.3.0-SNAPSHOT</version>
+        </dependency>
+
+        <!-- 参数校验 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- RPC 远程调用相关 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <!-- 设置构建的 jar 包名 -->
+        <finalName>${project.artifactId}</finalName>
+        <pluginManagement>
+            <plugins>
+                <!-- maven-surefire-plugin 插件,用于运行单元测试。 -->
+                <!-- 注意,需要使用 3.0.X+,因为要支持 Junit 5 版本 -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>${maven-surefire-plugin.version}</version>
+                </plugin>
+                <!-- maven-compiler-plugin 插件,解决 Lombok + MapStruct 组合 -->
+                <!-- https://stackoverflow.com/questions/33483697/re-run-spring-boot-configuration-annotation-processor-to-update-generated-metada -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>${maven-compiler-plugin.version}</version>
+                    <configuration>
+                        <annotationProcessorPaths>
+                            <path>
+                                <groupId>org.springframework.boot</groupId>
+                                <artifactId>spring-boot-configuration-processor</artifactId>
+                                <version>${spring.boot.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.projectlombok</groupId>
+                                <artifactId>lombok</artifactId>
+                                <version>${lombok.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.mapstruct</groupId>
+                                <artifactId>mapstruct-processor</artifactId>
+                                <version>${mapstruct.version}</version>
+                            </path>
+                        </annotationProcessorPaths>
+                        <!-- 编译参数写在 arg 内,解决 Spring Boot 3.2 的 Parameter Name Discovery 问题 -->
+                        <debug>false</debug>
+                        <compilerArgs>
+                            <arg>-parameters</arg>
+                        </compilerArgs>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+
+        <plugins>
+            <!-- 统一 revision 版本 -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>flatten-maven-plugin</artifactId>
+                <version>${flatten-maven-plugin.version}</version>
+                <configuration>
+                    <flattenMode>oss</flattenMode>
+                    <updatePomFile>true</updatePomFile>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>flatten</goal>
+                        </goals>
+                        <id>flatten</id>
+                        <phase>process-resources</phase>
+                    </execution>
+                    <execution>
+                        <goals>
+                            <goal>clean</goal>
+                        </goals>
+                        <id>flatten.clean</id>
+                        <phase>clean</phase>
+                    </execution>
+                </executions>
+            </plugin>
+            <!-- 关键:添加 spring-boot-maven-plugin -->
+            <!--<plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring.boot.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>-->
+        </plugins>
+    </build>
+
+</project>

+ 0 - 0
sikey-websocket-business/sikey-websocket-business-api/src/main/java/cn/sikey/websocket/api/test.json


+ 23 - 0
sikey-websocket-business/sikey-websocket-business-api/src/main/java/cn/sikey/websocket/enums/ApiConstants.java

@@ -0,0 +1,23 @@
+package cn.sikey.websocket.enums;
+
+import cn.sikey.framework.common.enums.RpcConstants;
+
+/**
+ * API 相关的枚举
+ *
+ * @author nelson
+ */
+public class ApiConstants {
+
+    /**
+     * 服务名
+     * <p>
+     * 注意,需要保证和 spring.application.name 保持一致
+     */
+    public static final String NAME = "sikey-websocket-business";
+
+    public static final String PREFIX = RpcConstants.RPC_API_PREFIX + "/websocket";
+
+    public static final String VERSION = "1.0.0";
+
+}

+ 24 - 0
sikey-websocket-business/sikey-websocket-business-biz/Dockerfile

@@ -0,0 +1,24 @@
+## 使用 Eclipse Temurin 的 JDK23 镜像
+FROM eclipse-temurin:23-jdk
+
+## 创建目录,并使用它作为工作目录
+RUN mkdir -p /sikey-websocket-business-biz
+WORKDIR /sikey-websocket-business-biz
+## 将后端项目的 Jar 文件,复制到镜像中
+COPY ./sikey-websocket-business-biz.jar sikey-websocket-business-biz.jar
+
+## 设置 TZ 时区
+ENV TZ=Asia/Shanghai JAVA_OPTS="-Xms512m -Xmx512m"
+
+## 创建日志目录并配置权限
+RUN mkdir -p /mnt/data/api-server/log && \
+    chmod 777 /mnt/data/api-server/log  # 确保容器用户有写入权限
+
+## 声明数据卷(与配置的日志路径一致)
+VOLUME /mnt/data/api-server/log
+
+## 暴露后端项目的 20003 端口
+EXPOSE 20003
+
+## 启动后端项目(保持原有启动命令)
+CMD java ${JAVA_OPTS} -Djava.security.egd=file:/dev/./urandom -jar sikey-websocket-business-biz.jar

+ 371 - 0
sikey-websocket-business/sikey-websocket-business-biz/pom.xml

@@ -0,0 +1,371 @@
+<?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">
+    <modelVersion>4.0.0</modelVersion>
+    <!-- Spring Boot 父级依赖 -->
+    <parent>
+        <groupId>cn.sikey.cloud</groupId>
+        <artifactId>sikey-websocket-business</artifactId>
+        <version>2.3.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>sikey-websocket-business-biz</artifactId>
+    <version>2.3.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <name>${project.artifactId}</name>
+    <description>
+        sikey-websocket-business-biz
+    </description>
+
+    <properties>
+        <java.version>23</java.version>
+        <maven.compiler.source>${java.version}</maven.compiler.source>
+        <maven.compiler.target>${java.version}</maven.compiler.target>
+        <consul.version>4.2.0</consul.version>
+        <spring.cloud.gateway.version>4.2.0</spring.cloud.gateway.version>
+        <!--<spring.cloud.alibaba.version>2023.0.1.0</spring.cloud.alibaba.version>-->
+        <lombok.version>1.18.36</lombok.version>
+        <logback.version>1.2.12</logback.version>
+        <mapstruct.version>1.6.2</mapstruct.version>
+        <spring.cloud.version>2024.0.0</spring.cloud.version>
+        <spring.boot.version>3.4.0</spring.boot.version>
+        <skywalking.version>9.0.0</skywalking.version>
+        <opentracing.version>0.33.0</opentracing.version>
+        <revision>2.3.0-SNAPSHOT</revision>
+
+        <maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>
+        <maven-compiler-plugin.version>3.13.0</maven-compiler-plugin.version>
+        <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
+        <okhttp3.version>4.12.0</okhttp3.version>
+        <hutool.version>5.8.28</hutool.version>
+        <google.zxing.version>3.5.2</google.zxing.version>
+        <alipay.v3.version>3.0.0.ALL</alipay.v3.version>
+        <wechat.v3.version>0.2.16</wechat.v3.version>
+        <gson.version>2.11.0</gson.version>
+        <security.version>3.4.4</security.version>
+        <commons-codec.version>1.15</commons-codec.version>
+    </properties>
+
+    <dependencies>
+
+        <!-- Web 开发基础依赖 -->
+        <dependency>
+            <groupId>cn.sikey.cloud</groupId>
+            <artifactId>sikey-spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- Registry 注册中心相关 -->
+        <!--<dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        </dependency>-->
+
+        <!-- Config 配置中心相关 -->
+        <!--<dependency>
+            <groupId>com.alibaba.cloud</groupId>
+            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        </dependency>-->
+
+        <!-- Spring Cloud Consul 配置中心 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-consul-config</artifactId>
+            <version>${consul.version}</version>
+        </dependency>
+
+        <!-- Spring Cloud Consul 服务发现 -->
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
+            <version>${consul.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.sikey.cloud</groupId>
+            <artifactId>sikey-common</artifactId>
+            <version>2.3.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>${lombok.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct</artifactId>
+            <version>${mapstruct.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-jdk8</artifactId>
+            <version>${mapstruct.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mapstruct</groupId>
+            <artifactId>mapstruct-processor</artifactId>
+            <version>${mapstruct.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>apm-toolkit-trace</artifactId>
+            <version>${skywalking.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>apm-toolkit-logback-1.x</artifactId>
+            <version>${skywalking.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.skywalking</groupId>
+            <artifactId>apm-toolkit-opentracing</artifactId>
+            <version>${skywalking.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.opentracing</groupId>
+            <artifactId>opentracing-api</artifactId>
+            <version>${opentracing.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.opentracing</groupId>
+            <artifactId>opentracing-util</artifactId>
+            <version>${opentracing.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.opentracing</groupId>
+            <artifactId>opentracing-noop</artifactId>
+            <version>${opentracing.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>${okhttp3.version}</version>
+        </dependency>
+
+        <!-- Hutool 核心库 -->
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-core</artifactId>
+            <version>${hutool.version}</version>
+        </dependency>
+
+        <!-- 显式添加 Gson -->
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>${gson.version}</version>
+        </dependency>
+
+        <!--<dependency>
+            <groupId>org.apache.tomcat.embed</groupId>
+            <artifactId>tomcat-embed-core</artifactId>
+        </dependency>-->
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>${commons-codec.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+            <version>1.5.12</version>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.5.12</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>2.0.16</version>
+        </dependency>
+
+        <!-- DB 相关 -->
+        <dependency>
+            <groupId>cn.sikey.cloud</groupId>
+            <artifactId>sikey-spring-boot-starter-mybatis</artifactId>
+        </dependency>
+
+        <!-- Redis 相关 -->
+        <dependency>
+            <groupId>cn.sikey.cloud</groupId>
+            <artifactId>sikey-spring-boot-starter-redis</artifactId>
+        </dependency>
+
+        <!-- Spring Boot AMQP Starter -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-amqp</artifactId>
+        </dependency>
+
+        <!-- Spring Boot WebSocket Starter -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>33.3.1-jre</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <!-- RPC 远程调用相关 -->
+        <!--<dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
+            <version>4.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+            <version>4.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>io.github.openfeign</groupId>
+            <artifactId>feign-okhttp</artifactId>
+            <version>12.5</version>
+        </dependency>-->
+
+        <!--<dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+            <version>3.4.4</version>
+        </dependency>-->
+
+
+        <!--<dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+            <version>${security.version}</version>
+        </dependency>-->
+    </dependencies>
+
+    <repositories>
+        <!--<repository>
+            <id>nexus-snapshots</id> &lt;!&ndash; 关键:此 ID 必须与 settings.xml 中的 <server> 的 <id> 一致 &ndash;&gt;
+            <url>http://106.75.230.4:18081/repository/maven-public/</url>
+        </repository>
+        <repository>
+            <id>nexus-releases</id> &lt;!&ndash; 关键:此 ID 必须与 settings.xml 中的 <server> 的 <id> 一致 &ndash;&gt;
+            <url>http://106.75.230.4:18081/repository/maven-public/</url>
+        </repository>-->
+
+        <!--<repository>
+            <id>sikey-group</id>
+            <url>http://106.75.230.4:18081/repository/sikey-group/</url>
+            <snapshots>
+                <enabled>true</enabled>
+            </snapshots>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+        </repository>-->
+
+    </repositories>
+
+    <build>
+        <!-- 设置构建的 jar 包名 -->
+        <finalName>${project.artifactId}</finalName>
+        <pluginManagement>
+            <plugins>
+                <!-- maven-surefire-plugin 插件,用于运行单元测试。 -->
+                <!-- 注意,需要使用 3.0.X+,因为要支持 Junit 5 版本 -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-surefire-plugin</artifactId>
+                    <version>${maven-surefire-plugin.version}</version>
+                </plugin>
+                <!-- maven-compiler-plugin 插件,解决 Lombok + MapStruct 组合 -->
+                <!-- https://stackoverflow.com/questions/33483697/re-run-spring-boot-configuration-annotation-processor-to-update-generated-metada -->
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>${maven-compiler-plugin.version}</version>
+                    <configuration>
+                        <annotationProcessorPaths>
+                            <path>
+                                <groupId>org.springframework.boot</groupId>
+                                <artifactId>spring-boot-configuration-processor</artifactId>
+                                <version>${spring.boot.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.projectlombok</groupId>
+                                <artifactId>lombok</artifactId>
+                                <version>${lombok.version}</version>
+                            </path>
+                            <path>
+                                <groupId>org.mapstruct</groupId>
+                                <artifactId>mapstruct-processor</artifactId>
+                                <version>${mapstruct.version}</version>
+                            </path>
+                        </annotationProcessorPaths>
+                        <!-- 编译参数写在 arg 内,解决 Spring Boot 3.2 的 Parameter Name Discovery 问题 -->
+                        <debug>false</debug>
+                        <compilerArgs>
+                            <arg>-parameters</arg>
+                        </compilerArgs>
+                    </configuration>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+
+        <plugins>
+            <!-- 统一 revision 版本 -->
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>flatten-maven-plugin</artifactId>
+                <version>${flatten-maven-plugin.version}</version>
+                <configuration>
+                    <flattenMode>oss</flattenMode>
+                    <updatePomFile>true</updatePomFile>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>flatten</goal>
+                        </goals>
+                        <id>flatten</id>
+                        <phase>process-resources</phase>
+                    </execution>
+                    <execution>
+                        <goals>
+                            <goal>clean</goal>
+                        </goals>
+                        <id>flatten.clean</id>
+                        <phase>clean</phase>
+                    </execution>
+                </executions>
+            </plugin>
+            <!-- 关键:添加 spring-boot-maven-plugin -->
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring.boot.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 25 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/WebsocketServerApplication.java

@@ -0,0 +1,25 @@
+package cn.sikey.websocket;
+
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+/**
+ * 项目的启动类
+ * <p>
+ *
+ * @author nelson
+ */
+@EnableDiscoveryClient
+@Slf4j
+@SpringBootApplication
+public class WebsocketServerApplication {
+
+    public static void main(String[] args) {
+        log.info("Starting WebsocketServerApplication");
+        SpringApplication.run(WebsocketServerApplication.class, args);
+    }
+
+}

+ 76 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/client/handler/MessageQueueHandler.java

@@ -0,0 +1,76 @@
+package cn.sikey.websocket.client.handler;
+
+import cn.hutool.json.JSONUtil;
+import cn.sikey.websocket.controller.app.message.vo.SaveMessageSendReqVO;
+import cn.sikey.websocket.enums.MessageStatus;
+import cn.sikey.websocket.enums.MessageType;
+import cn.sikey.websocket.message.BasicMessage;
+import cn.sikey.websocket.message.Message;
+import cn.sikey.websocket.message.MessageId;
+import cn.sikey.websocket.message.MessageReceivedLog;
+import cn.sikey.websocket.service.MessageSendService;
+import cn.sikey.websocket.service.MessagingSubscribeService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.rabbit.annotation.RabbitHandler;
+import org.springframework.amqp.rabbit.annotation.RabbitListener;
+import org.springframework.messaging.handler.annotation.Headers;
+import org.springframework.messaging.handler.annotation.Payload;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: 收到mq消息
+ */
+@Slf4j
+@Component
+@RabbitListener(queues = "publish_mcdisk_queue")
+public class MessageQueueHandler {
+
+    @Resource
+    private MessagingSubscribeService messagingSubscribeService;
+
+    @Resource
+    private MessageSendService messageSendService;
+
+    /**
+     * 接收聊天文本消息
+     *
+     * @param chatTextMessage 聊天文本消息
+     */
+    @RabbitHandler
+    public void receiveChatTextMessage(@Payload BasicMessage chatTextMessage) {
+        log.info("收到mq消息: {}", JSONUtil.toJsonPrettyStr(chatTextMessage));
+        Message message = convertMessage(chatTextMessage);
+        messagingSubscribeService.consumeMessage(message);
+    }
+
+    /**
+     * 转换消息
+     *
+     * @param basicMessage 基础消息
+     * @return 消息
+     */
+    private Message convertMessage(BasicMessage basicMessage) {
+        MessageId messageId = new MessageId(basicMessage.getMsgId());
+        Message message = find(messageId);
+        message.setReceiverId(List.of(basicMessage.getRecvId()));
+        message.setReceivedLogs(
+                List.of(new MessageReceivedLog().setReceiverId(basicMessage.getRecvId()).setStatus(MessageStatus.DEFAULT)));
+        return message;
+    }
+
+    private Message find(MessageId messageId) {
+        SaveMessageSendReqVO messageSendModel = messageSendService.findMessageSendById(messageId);
+        return new Message()
+                .setId(messageId)
+                .setType(MessageType.valueOf(messageSendModel.getMsgType()))
+                .setSenderId(messageSendModel.getSenderId())
+                .setSendTime(messageSendModel.getSendTime())
+                .setContent(messageSendModel.getContent());
+    }
+}

+ 99 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/client/handler/WebSocketHandler.java

@@ -0,0 +1,99 @@
+package cn.sikey.websocket.client.handler;
+
+import cn.hutool.json.JSONUtil;
+import cn.sikey.websocket.websocket.ConnectionHeartbeatService;
+import cn.sikey.websocket.websocket.ConnectionManagerService;
+import io.netty.handler.codec.UnsupportedMessageTypeException;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.PongMessage;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+import java.io.IOException;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: webSocket处理器
+ */
+@Component
+@Slf4j
+public class WebSocketHandler extends TextWebSocketHandler {
+
+    @Resource
+    private ConnectionHeartbeatService connectionHeartbeatService;
+
+    @Resource
+    private ConnectionManagerService connectionManagerService;
+
+    private String getSecWebSocketKey(WebSocketSession session) {
+        return session.getHandshakeHeaders().getFirst("Sec-WebSocket-Key");
+    }
+
+    /**
+     * 处理消息
+     *
+     * @param webSocketSession 会话
+     * @param textMessage      消息
+     */
+    @Override
+    protected void handleTextMessage(WebSocketSession webSocketSession, TextMessage textMessage) throws Exception {
+        log.info("[处理消息]消息内容:{}", JSONUtil.toJsonStr(textMessage.getPayload()));
+        connectionHeartbeatService.handleCustomizedMessage(webSocketSession);
+    }
+
+    /**
+     * 建立连接
+     *
+     * @param webSocketSession 会话
+     * @throws Exception 异常
+     */
+    @Override
+    public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
+        connectionManagerService.connectionEstablished(webSocketSession);
+    }
+
+    /**
+     * 关闭连接
+     *
+     * @param webSocketSession 会话
+     * @param closeStatus      状态
+     * @throws Exception 异常
+     */
+    @Override
+    public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
+        connectionManagerService.connectionClosed(webSocketSession, closeStatus);
+    }
+
+    /**
+     * 处理Pong
+     *
+     * @param webSocketSession 会话
+     * @param pongMessage      消息
+     * @throws Exception 异常
+     */
+    @Override
+    protected void handlePongMessage(WebSocketSession webSocketSession, PongMessage pongMessage) throws Exception {
+        connectionHeartbeatService.handlePongMessage(webSocketSession, pongMessage);
+    }
+
+    /**
+     * 处理Ping
+     *
+     * @param session   会话
+     * @param exception 异常
+     * @return
+     */
+    @Override
+    public void handleTransportError(WebSocketSession session, Throwable exception) throws IOException {
+        if (exception instanceof UnsupportedMessageTypeException) {
+            log.debug("Received Ping:{}", JSONUtil.toJsonPrettyStr(session));
+            // 处理Ping消息
+            session.sendMessage(new PongMessage());
+        }
+    }
+}

+ 28 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/client/interceptor/WebSocketInterceptor.java

@@ -0,0 +1,28 @@
+package cn.sikey.websocket.client.interceptor;
+
+import cn.sikey.websocket.client.handler.WebSocketHandler;
+import cn.sikey.websocket.config.WebSocketConfig;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+
+@Configuration
+@EnableWebSocket
+public class WebSocketInterceptor implements WebSocketConfigurer {
+    private final WebSocketHandler webSocketHandler;
+
+    private final WebSocketConfig webSocketConfig;
+
+    public WebSocketInterceptor(WebSocketHandler webSocketHandler, WebSocketConfig webSocketConfig) {
+        this.webSocketHandler = webSocketHandler;
+        this.webSocketConfig = webSocketConfig;
+    }
+
+    @Override
+    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+        registry.addHandler(webSocketHandler, "/websocket/app/connect/websocket")
+                .setAllowedOriginPatterns("*");
+    }
+
+}

+ 180 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/config/CacheConnectionStateManager.java

@@ -0,0 +1,180 @@
+package cn.sikey.websocket.config;
+
+import cn.sikey.websocket.enums.MessageType;
+import cn.sikey.websocket.message.HeartbeatMessage;
+import cn.sikey.websocket.websocket.Connection;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisOperations;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Objects;
+
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: 缓存连接状态管理
+ */
+@Slf4j
+@Component
+public class CacheConnectionStateManager {
+    private static final String CONNECTION_KEY = "connection.mcdisk.";
+    private final ObjectMapper objectMapper = initializeObjectMapper();
+
+    private final RedisOperations<String, String> operations;
+    private final WebSocketConfig webSocketConfig;
+
+    @Autowired
+    public CacheConnectionStateManager(RedisOperations<String, String> operations, WebSocketConfig webSocketConfig) {
+        this.operations = operations;
+        this.webSocketConfig = webSocketConfig;
+    }
+
+    private final Cache<String, Entry> cache = CacheBuilder
+            .newBuilder()
+            .maximumSize(10000L)
+            .build();
+
+    private record Entry(Connection connection, WebSocketSession session) {
+    }
+
+    /**
+     * 初始化ObjectMapper
+     *
+     * @return ObjectMapper
+     */
+    private ObjectMapper initializeObjectMapper() {
+        ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder()
+                .createXmlMapper(false)
+                .failOnEmptyBeans(false)
+                .failOnUnknownProperties(false)
+                .build();
+        objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
+        return objectMapper;
+    }
+
+    /**
+     * 下线
+     *
+     * @param connection 连接
+     */
+    public void offline(Connection connection) {
+        try {
+            Objects.requireNonNull(cache.getIfPresent(connection.getId())).session.close(CloseStatus.NORMAL);
+            cache.invalidate(connection.getId());
+            operations.delete(buildConnectionKey(connection.getId()));
+        } catch (IOException e) {
+            log.error("Close session error", e);
+        }
+    }
+
+    /**
+     * 上线
+     *
+     * @param connection 上线
+     * @param session    WebSocket 会话
+     * @throws Exception 异常
+     */
+    public void online(Connection connection, WebSocketSession session) throws Exception {
+        cache.put(connection.getId(), new Entry(connection, session));
+        operations.opsForValue().set(buildConnectionKey(connection.getId()), objectMapper.writeValueAsString(connection),
+                webSocketConfig.getHeartbeatTimeout().plus(webSocketConfig.getCheckTimeout()));
+    }
+
+    /**
+     * 是否在线
+     *
+     * @return 是否在线
+     */
+    public boolean isOnline(String userId) {
+        return operations.opsForValue().get(buildConnectionKey(userId)) != null;
+    }
+
+    /**
+     * 心跳
+     *
+     * @param connection 连接
+     */
+    public void heartbeat(Connection connection) throws IOException {
+        Entry entry = cache.getIfPresent(connection.getId());
+        if (entry != null) {
+            entry.connection.setLastHeartbeat(Instant.now());
+            cache.put(connection.getId(), entry);
+            operations.expire(buildConnectionKey(connection.getId()),
+                    webSocketConfig.getHeartbeatTimeout().plus(webSocketConfig.getCheckTimeout()));
+            HeartbeatMessage message = buildHeartbeatMessage();
+            final ObjectMapper cleanFieldObjectMapper = objectMapper.copy();
+            cleanFieldObjectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+            String messageString = cleanFieldObjectMapper.writeValueAsString(message);
+            entry.session().sendMessage(new TextMessage(messageString));
+        }
+    }
+
+    /**
+     * 获取心跳消息
+     *
+     * @return {@link HeartbeatMessage}
+     */
+    private HeartbeatMessage buildHeartbeatMessage() {
+        HeartbeatMessage message = new HeartbeatMessage();
+        message.setMsgType(MessageType.HEARTBEAT_MESSAGES)
+                .setSendTime(LocalDateTime.now());
+        message.setContent(new HeartbeatMessage.HeartbeatContent().setRaw("pong"));
+        return message;
+    }
+
+    /**
+     * 获取所有连接
+     *
+     * @return 连接列表
+     */
+    public List<Connection> getConnections() {
+        return List.copyOf(cache.asMap().values().stream().map(Entry::connection).toList());
+    }
+
+    /**
+     * 获取连接
+     *
+     * @param userId 连接ID
+     * @return 连接
+     */
+    public Connection getConnection(String userId) {
+        Entry entry = cache.getIfPresent(userId);
+        return entry == null ? null : entry.connection;
+    }
+
+    /**
+     * 获取会话
+     *
+     * @param userId 用户ID
+     * @return 会话
+     */
+    public WebSocketSession getSession(String userId) {
+        Entry entry = cache.getIfPresent(userId);
+        return entry == null ? null : entry.session;
+    }
+
+    /**
+     * 构建连接Key
+     *
+     * @param userId 用户ID
+     * @return 连接Key
+     */
+    private String buildConnectionKey(String userId) {
+        return CONNECTION_KEY + userId;
+    }
+}

+ 34 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/config/MessagingRabbitmqConfig.java

@@ -0,0 +1,34 @@
+package cn.sikey.websocket.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Data;
+import org.springframework.amqp.rabbit.annotation.EnableRabbit;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.MessageConverter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: mq配置
+ */
+@Data
+@Configuration
+@EnableRabbit
+@ConfigurationProperties(prefix = "spring.rabbitmq.messaging")
+public class MessagingRabbitmqConfig {
+    private String queueName;
+    private String exchangeName;
+    private String routingKey;
+
+    @Bean
+    public MessageConverter createMessageConverter() {
+        ObjectMapper mapper = new Jackson2ObjectMapperBuilder().createXmlMapper(false).failOnEmptyBeans(false).failOnUnknownProperties(false).build();
+        mapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
+        return new Jackson2JsonMessageConverter(mapper);
+    }
+}

+ 36 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/config/RabbitMQMessaging.java

@@ -0,0 +1,36 @@
+package cn.sikey.websocket.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.MessagePostProcessor;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.stereotype.Component;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: mq消息发送
+ */
+@Slf4j
+@Component
+public class RabbitMQMessaging {
+    private final RabbitTemplate rabbitTemplate;
+    private final MessagingRabbitmqConfig messagingRabbitmqConfig;
+
+    public RabbitMQMessaging(RabbitTemplate rabbitTemplate, MessagingRabbitmqConfig messagingRabbitmqConfig) {
+        this.rabbitTemplate = rabbitTemplate;
+        this.messagingRabbitmqConfig = messagingRabbitmqConfig;
+    }
+
+    /**
+     * mq消息发送
+     *
+     * @param message 消息体
+     * @return
+     */
+    public void publish(Object message) {
+        log.info("mq消息发送,消息:{}", message);
+        MessagePostProcessor messagePostProcessor = messagePost -> messagePost;
+        rabbitTemplate.convertAndSend(
+                messagingRabbitmqConfig.getExchangeName(), messagingRabbitmqConfig.getRoutingKey(), message, messagePostProcessor);
+    }
+}

+ 33 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/config/ThreadPoolConfig.java

@@ -0,0 +1,33 @@
+package cn.sikey.websocket.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: 线程池
+ */
+@Configuration
+public class ThreadPoolConfig {
+    @Bean(name = "myTaskExecutor")
+    public ThreadPoolTaskExecutor taskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        // 核心线程数(默认线程数)
+        executor.setCorePoolSize(5);
+        // 最大线程数
+        executor.setMaxPoolSize(10);
+        // 队列容量(当核心线程满时,新任务进入队列)
+        executor.setQueueCapacity(100);
+        // 线程池名前缀
+        executor.setThreadNamePrefix("my-async-thread");
+        // 拒绝策略:由调用者线程(主线程)执行任务
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        // 初始化线程池
+        executor.initialize();
+        return executor;
+    }
+}

+ 31 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/config/WebSocketConfig.java

@@ -0,0 +1,31 @@
+package cn.sikey.websocket.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.convert.DurationUnit;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.annotation.Configuration;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+
+@Data
+@Configuration
+@RefreshScope
+@ConfigurationProperties("websocket")
+public class WebSocketConfig {
+    /**
+     * 心跳间隔时长, 单位秒
+     */
+    @DurationUnit(ChronoUnit.SECONDS)
+    private Duration heartbeatTimeout;
+
+    /**
+     * 检查超时
+     * <br/>
+     * 检测连接是否活跃的定时器会在10s执行一次,这里建议也设置10s
+     */
+    @DurationUnit(ChronoUnit.SECONDS)
+    private Duration checkTimeout;
+
+}

+ 37 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/message/MessageController.java

@@ -0,0 +1,37 @@
+package cn.sikey.websocket.controller.app.message;
+
+import cn.sikey.framework.common.pojo.CommonResult;
+import cn.sikey.websocket.controller.app.message.vo.MessagingPublishReqVO;
+import cn.sikey.websocket.controller.app.message.vo.MessagingPublishRespVO;
+import cn.sikey.websocket.service.MessagingPublishService;
+import jakarta.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: 消息
+ */
+@RestController
+@RequestMapping("/websocket/app/messaging/publish")
+public class MessageController {
+
+    @Resource
+    private MessagingPublishService messagingPublishService;
+
+    /**
+     * 测试消息
+     *
+     * @param request 请求
+     * @return
+     */
+    @PostMapping("/test")
+    public CommonResult test(@Validated @RequestBody MessagingPublishReqVO<MessagingPublishReqVO.TestContent> request) {
+        MessagingPublishRespVO messagingPublishRespVO = messagingPublishService.publish(request);
+        return CommonResult.success(messagingPublishRespVO);
+    }
+}

+ 55 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/message/vo/MessagingPublishReqVO.java

@@ -0,0 +1,55 @@
+package cn.sikey.websocket.controller.app.message.vo;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: 消息发布
+ */
+@Data
+@Accessors(chain = true)
+public class MessagingPublishReqVO<T> implements Serializable {
+    @Serial
+    private static final long serialVersionUID = -9151951858562015064L;
+
+    /**
+     * 消息类型
+     */
+    @NotNull(message = "消息类型不能为空")
+    private Integer msgType;
+
+    /**
+     * 消息发送的时间
+     */
+    @NotNull(message = "发送时间不能为空")
+    private LocalDateTime sendTime;
+
+    /**
+     * 接收者ID
+     */
+    @NotBlank(message = "接收者ID不能为空")
+    private String recvId;
+
+    /**
+     * 消息内容
+     */
+    @NotNull(message = "消息内容不能为空")
+    private T content;
+
+    @Data
+    @Accessors(chain = true)
+    public static class TestContent implements Serializable {
+        @Serial
+        private static final long serialVersionUID = -9151951858562015064L;
+
+    }
+
+}

+ 20 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/message/vo/MessagingPublishRespVO.java

@@ -0,0 +1,20 @@
+package cn.sikey.websocket.controller.app.message.vo;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: 消息返回值
+ */
+@Data
+@Accessors(chain = true)
+public class MessagingPublishRespVO implements Serializable {
+    /**
+     * 消息ID
+     */
+    private Long msgId;
+}

+ 50 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/message/vo/SaveMessageRecvReqVO.java

@@ -0,0 +1,50 @@
+package cn.sikey.websocket.controller.app.message.vo;
+
+import cn.hutool.json.JSONObject;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/2
+ * @Description: 消息发送接收
+ */
+@Data
+@Accessors(chain = true)
+public class SaveMessageRecvReqVO {
+
+    /**
+     * id
+     */
+    private Long id;
+    /**
+     * 消息id
+     */
+    private Long msgId;
+    /**
+     * 接收人
+     */
+    private String recvId;
+    /**
+     * 发送人
+     */
+    private String senderId;
+    /**
+     * 发送时间
+     */
+    private LocalDateTime sendTime;
+    /**
+     * 消息类型
+     */
+    private Integer msgType;
+    /**
+     * 消息内容
+     */
+    private JSONObject content;
+    /**
+     * 消息状态
+     */
+    private Integer msgStatus;
+}

+ 37 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/message/vo/SaveMessageSendReqVO.java

@@ -0,0 +1,37 @@
+package cn.sikey.websocket.controller.app.message.vo;
+
+import cn.hutool.json.JSONObject;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/2
+ * @Description: 保存消息发送
+ */
+@Data
+@Accessors(chain = true)
+public class SaveMessageSendReqVO {
+    /**
+     * id
+     */
+    private Long id;
+    /**
+     * 发送人id
+     */
+    private String senderId;
+    /**
+     * 消息类型
+     */
+    private Integer msgType;
+    /**
+     * 消息内容
+     */
+    private JSONObject content;
+    /**
+     * 发送时间
+     */
+    private LocalDateTime sendTime;
+}

+ 0 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/app/test.json


+ 0 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/controller/test.json


+ 24 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/convert/MessageConvert.java

@@ -0,0 +1,24 @@
+package cn.sikey.websocket.convert;
+
+import cn.sikey.websocket.controller.app.message.vo.SaveMessageRecvReqVO;
+import cn.sikey.websocket.controller.app.message.vo.SaveMessageSendReqVO;
+import cn.sikey.websocket.dal.dataobject.MessageRecvDO;
+import cn.sikey.websocket.dal.dataobject.MessageSendDO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/1
+ * @Description: 消息
+ */
+@Mapper
+public interface MessageConvert {
+    MessageConvert INSTANCE = Mappers.getMapper(MessageConvert.class);
+
+    MessageSendDO convertMessageSendDO(SaveMessageSendReqVO saveMessageSendReqVO);
+
+    MessageRecvDO convertMessageRecvDO(SaveMessageRecvReqVO saveMessageRecvReqVO);
+
+    SaveMessageSendReqVO convertSaveMessageSendReqVO(MessageSendDO  messageSendDO);
+}

+ 57 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/dal/dataobject/MessageRecvDO.java

@@ -0,0 +1,57 @@
+package cn.sikey.websocket.dal.dataobject;
+
+
+import cn.hutool.json.JSONObject;
+import cn.sikey.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * 消息发送接收
+ *
+ * @author nelson
+ */
+@TableName("mc_message_recv")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class MessageRecvDO extends BaseDO {
+
+    /**
+     * id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 消息id
+     */
+    private Long msgId;
+    /**
+     * 接收人
+     */
+    private String recvId;
+    /**
+     * 发送人
+     */
+    private String senderId;
+    /**
+     * 发送时间
+     */
+    private LocalDateTime sendTime;
+    /**
+     * 消息类型
+     */
+    private Integer msgType;
+    /**
+     * 消息内容
+     */
+    private JSONObject content;
+    /**
+     * 消息状态
+     */
+    private Integer msgStatus;
+}

+ 44 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/dal/dataobject/MessageSendDO.java

@@ -0,0 +1,44 @@
+package cn.sikey.websocket.dal.dataobject;
+
+
+import cn.hutool.json.JSONObject;
+import cn.sikey.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDateTime;
+
+/**
+ * 消息发送
+ *
+ * @author nelson
+ */
+@TableName("mc_message_send")
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class MessageSendDO extends BaseDO {
+
+    /**
+     * id
+     */
+    @TableId
+    private Long id;
+    /**
+     * 发送人id
+     */
+    private String senderId;
+    /**
+     * 消息类型
+     */
+    private Integer msgType;
+    /**
+     * 消息内容
+     */
+    private JSONObject content;
+    /**
+     * 发送时间
+     */
+    private LocalDateTime sendTime;
+}

+ 28 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/dal/mysql/MessageRecvMapper.java

@@ -0,0 +1,28 @@
+package cn.sikey.websocket.dal.mysql;
+
+import cn.sikey.websocket.dal.dataobject.MessageRecvDO;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 消息发送接收
+ *
+ * @author nelson
+ */
+@Mapper
+public interface MessageRecvMapper extends BaseMapper<MessageRecvDO> {
+
+    default void saveMessageRecv(MessageRecvDO messageRecvDO) {
+        insert(messageRecvDO);
+    }
+
+    default void updateStatusById(Integer status, Long ackId) {
+        UpdateWrapper<MessageRecvDO> updateWrapper = new UpdateWrapper<>();
+        updateWrapper.eq("id", ackId);
+        updateWrapper.set("status", status);
+        update(updateWrapper);
+    }
+}
+
+

+ 28 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/dal/mysql/MessageSendMapper.java

@@ -0,0 +1,28 @@
+package cn.sikey.websocket.dal.mysql;
+
+import cn.sikey.websocket.dal.dataobject.MessageSendDO;
+import cn.sikey.websocket.message.MessageId;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 消息发送
+ *
+ * @author nelson
+ */
+@Mapper
+public interface MessageSendMapper extends BaseMapper<MessageSendDO> {
+
+    default void saveMessageSend(MessageSendDO messageSendDO) {
+        insert(messageSendDO);
+    }
+
+    default MessageSendDO findById(MessageId messageId) {
+        QueryWrapper<MessageSendDO> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("msg_id", messageId);
+        return selectOne(queryWrapper);
+    }
+}
+
+

+ 52 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/enums/MessageStatus.java

@@ -0,0 +1,52 @@
+package cn.sikey.websocket.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: 消息状态
+ */
+@AllArgsConstructor
+@Getter
+public enum MessageStatus {
+
+    /**
+     * 未读 (初始状态)
+     */
+    DEFAULT(0),
+
+    /**
+     * 离线队列
+     */
+    OFFLINE_QUEUE(3),
+
+    /**
+     * 等待 ACK
+     */
+    WAIT_ACK(8),
+
+    /**
+     * 已收到 ACK
+     */
+    RECEIVED_ACK(9),
+
+    /**
+     * 已读
+     */
+    READ(10);
+
+    private final Integer value;
+
+    public static MessageStatus valueOf(Integer value) {
+        MessageStatus[] statuses = MessageStatus.values();
+        for (MessageStatus status : statuses) {
+            if (status.getValue().equals(value)) {
+                return status;
+            }
+        }
+        throw new RuntimeException("Invalid value");
+    }
+
+}

+ 51 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/enums/MessageType.java

@@ -0,0 +1,51 @@
+package cn.sikey.websocket.enums;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/30
+ * @Description: 消息类型
+ */
+@AllArgsConstructor
+@Getter
+@JsonFormat(shape = JsonFormat.Shape.OBJECT)
+public enum MessageType {
+    /**
+     * 心跳消息, 保持活跃的消息
+     */
+    HEARTBEAT_MESSAGES(1, ""),
+    /**
+     * ack 消息,服务器和客户端双方约定收到消息后的回执消息
+     */
+    ACK_MESSAGES(2, ""),
+    /**
+     * test文本消息
+     */
+    TEST_TEXT_MESSAGE(10, "publish.test.text"),
+
+    ;
+
+    @JsonValue
+    private final Integer value;
+
+    private final String routingKey;
+
+    /**
+     * 根据 value 获取 MessageType
+     *
+     * @param value value
+     * @return MessageType
+     */
+    public static MessageType valueOf(Integer value) {
+        for (MessageType messageType : MessageType.values()) {
+            if (messageType.value.equals(value)) {
+                return messageType;
+            }
+        }
+        throw new RuntimeException("Invalid MessageType value: " + value);
+    }
+}

+ 57 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/BasicMessage.java

@@ -0,0 +1,57 @@
+package cn.sikey.websocket.message;
+
+import cn.sikey.websocket.enums.MessageType;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+/**
+ * 基础消息
+ */
+@Data
+@Accessors(chain = true)
+@ToString(callSuper = true)
+public class BasicMessage {
+
+    /**
+     * 消息类型
+     *
+     */
+    private MessageType msgType;
+
+    /**
+     * 应答ID <br/>
+     * Websocket 服务在将消息发送之前,需要查询 message_recv 表将数据带上
+     */
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private Long ackId;
+
+    /**
+     * 消息ID
+     */
+    private Long msgId;
+
+    /**
+     * 接收人ID
+     */
+    private String recvId;
+
+    /**
+     * 发送人ID
+     */
+    private String senderId;
+
+    /**
+     * 发送时间
+     */
+    private LocalDateTime sendTime;
+
+    /**
+     * 消息内容 <br/>
+     * 子类一般会重载这个字段
+     */
+    private Object content;
+}

+ 18 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/HeartbeatMessage.java

@@ -0,0 +1,18 @@
+package cn.sikey.websocket.message;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+public class HeartbeatMessage extends BasicMessage {
+    private HeartbeatContent content;
+
+    @Data
+    @Accessors(chain = true)
+    public static class HeartbeatContent {
+        private String raw;
+    }
+}

+ 64 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/Message.java

@@ -0,0 +1,64 @@
+package cn.sikey.websocket.message;
+
+import cn.sikey.websocket.enums.MessageType;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 消息
+ */
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+@ToString
+public class Message {
+
+    /**
+     * 消息ID
+     */
+    private MessageId id;
+
+    /**
+     * 消息类型
+     */
+    private MessageType type;
+
+    /**
+     * 消息类容
+     */
+    private Object content;
+
+    /**
+     * 消息发送人
+     */
+    private String senderId;
+
+    /**
+     * 发送时间
+     */
+    private LocalDateTime sendTime;
+
+    /**
+     * 消息读取记录
+     */
+    private List<MessageReceivedLog> receivedLogs;
+
+    /**
+     * 消息读取人列表
+     */
+    private List<String> receiverId;
+
+    @Override
+    protected Object clone() throws CloneNotSupportedException {
+        return super.clone();
+    }
+
+}

+ 26 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/MessageId.java

@@ -0,0 +1,26 @@
+package cn.sikey.websocket.message;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 消息id
+ */
+@Data
+@Accessors(chain = true)
+@AllArgsConstructor
+public class MessageId implements Comparable<MessageId> {
+    private Long id;
+
+    @Override
+    public int compareTo(MessageId o) {
+        return o.getId().compareTo(getId());
+    }
+
+    public boolean greaterThan(Long id) {
+        return this.id > id;
+    }
+}

+ 41 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/MessageReceivedLog.java

@@ -0,0 +1,41 @@
+package cn.sikey.websocket.message;
+
+import cn.sikey.websocket.enums.MessageStatus;
+import lombok.*;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 消息接受记录
+ */
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+@ToString
+@AllArgsConstructor
+@NoArgsConstructor
+public class MessageReceivedLog {
+
+    /**
+     * 消息读取人
+     */
+    private String receiverId;
+
+    /**
+     * ack id
+     */
+    private Long ackId;
+
+    /**
+     * 消息状态
+     */
+    private MessageStatus status;
+
+    /**
+     * 读取消息时间
+     */
+    private LocalDateTime receivedTime;
+}

+ 34 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/message/TestTextMessage.java

@@ -0,0 +1,34 @@
+package cn.sikey.websocket.message;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: test
+ */
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+@ToString(callSuper = true)
+public class TestTextMessage extends BasicMessage implements Serializable {
+    @Serial
+    private static final long serialVersionUID = -7474476471144411028L;
+
+    /**
+     * 消息内容
+     */
+    private TestTextMessageContent content;
+
+    @Data
+    @Accessors(chain = true)
+    public static class TestTextMessageContent {
+        private String raw;
+    }
+}

+ 23 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/scheduler/KeepItAliveScheduler.java

@@ -0,0 +1,23 @@
+package cn.sikey.websocket.scheduler;
+
+import cn.sikey.websocket.websocket.ConnectionManagerService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class KeepItAliveScheduler {
+
+    @Resource
+    private ConnectionManagerService connectionManagerService;
+
+    /**
+     * 检查Session上次活跃时间 <b>10秒检查一次</b>
+     */
+    @Scheduled(cron = "*/3 * * * * *")
+    public void run() {
+        connectionManagerService.removeDeadConnection();
+    }
+}

+ 125 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/AbstractWebSocketService.java

@@ -0,0 +1,125 @@
+package cn.sikey.websocket.service;
+
+import cn.sikey.websocket.config.CacheConnectionStateManager;
+import cn.sikey.websocket.config.RabbitMQMessaging;
+import cn.sikey.websocket.controller.app.message.vo.MessagingPublishReqVO;
+import cn.sikey.websocket.controller.app.message.vo.MessagingPublishRespVO;
+import cn.sikey.websocket.controller.app.message.vo.SaveMessageRecvReqVO;
+import cn.sikey.websocket.enums.MessageStatus;
+import cn.sikey.websocket.enums.MessageType;
+import cn.sikey.websocket.message.Message;
+import cn.sikey.websocket.message.MessageId;
+import cn.sikey.websocket.message.MessageReceivedLog;
+import cn.sikey.websocket.util.MessageConverterUtil;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 抽象服务
+ */
+@Slf4j
+public class AbstractWebSocketService {
+
+    @Resource
+    private MessageIdIncrementingService<Long> messageIdIncrementingService;
+
+    @Resource
+    private MessageSendService messageSendService;
+
+    @Resource
+    private RabbitMQMessaging rabbitMQMessaging;
+
+    @Resource
+    private CacheConnectionStateManager cacheConnectionStateManager;
+
+    @Resource
+    private MessageRecvService messageRecvService;
+
+    @Resource
+    private ObjectMapper objectMapper;
+
+    /**
+     * 发布消息
+     *
+     * @param request 请求
+     * @param <T>     消息内容类型
+     * @return
+     */
+    public <T> MessagingPublishRespVO publish(MessagingPublishReqVO<T> request) {
+        MessageType messageType = MessageType.valueOf(request.getMsgType());
+
+        Message message = new Message().setId(new MessageId(messageIdIncrementingService.incr())).setType(messageType).setContent(request.getContent()).
+                setSenderId("test").setSendTime(request.getSendTime());
+        message.setReceiverId(List.of(request.getRecvId()));
+        messageSendService.saveMessageSend(message);
+
+        // 过滤用户自己
+        message.setReceiverId(message.getReceiverId().stream().filter(r -> !r.equals("test")).toList());
+
+        message.getReceiverId().forEach(receiverId -> {
+            // 接收消息用户
+            message.setReceiverId(List.of(receiverId));
+            Object messagingObject = MessageConverterUtil.toMessaging(message);
+            log.info("发消息给接收者: {},消息内容:{}", receiverId, messagingObject);
+            rabbitMQMessaging.publish(messagingObject);
+        });
+
+        return new MessagingPublishRespVO().setMsgId(message.getId().getId());
+    }
+
+    /**
+     * 消费消息
+     *
+     * @param message 消息
+     * @return
+     */
+    public void consumeMessage(Message message) {
+
+        try {
+            for (MessageReceivedLog messageReceivedLog : message.getReceivedLogs()) {
+                String receiverId = messageReceivedLog.getReceiverId();
+
+                // 加入数据库并且获取 ackId
+                SaveMessageRecvReqVO saveMessageRecvReqVO = new SaveMessageRecvReqVO();
+                saveMessageRecvReqVO.setMsgId(message.getId().getId());
+                saveMessageRecvReqVO.setRecvId(receiverId);
+                saveMessageRecvReqVO.setSenderId(message.getSenderId());
+                saveMessageRecvReqVO.setMsgType(message.getType().getValue());
+                saveMessageRecvReqVO.setContent(objectMapper.convertValue(message.getContent(), new TypeReference<>() {
+                }));
+                saveMessageRecvReqVO.setSendTime(message.getSendTime());
+                saveMessageRecvReqVO.setMsgStatus(MessageStatus.DEFAULT.getValue());
+
+                messageRecvService.saveMessageRecv(saveMessageRecvReqVO);
+                messageReceivedLog.setAckId(saveMessageRecvReqVO.getId());
+
+                if (cacheConnectionStateManager.isOnline(receiverId)) {
+
+                    WebSocketSession session = cacheConnectionStateManager.getSession(receiverId);
+                    if (Objects.nonNull(session)) {
+                        session.sendMessage(
+                                new TextMessage(objectMapper.writeValueAsString(MessageConverterUtil.toMessaging(message))));
+                        messageRecvService.updateStatusById(MessageStatus.WAIT_ACK.getValue(), messageReceivedLog.getAckId());
+                        continue;
+                    }
+                }
+
+                // 消息存入离线队列
+                messageRecvService.updateStatusById(MessageStatus.OFFLINE_QUEUE.getValue(), messageReceivedLog.getAckId());
+            }
+
+            log.info("消费消息完成");
+        } catch (Exception e) {
+        }
+    }
+
+}

+ 23 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageIdIncrementingService.java

@@ -0,0 +1,23 @@
+package cn.sikey.websocket.service;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 消息id递增适配器
+ */
+public interface MessageIdIncrementingService<ID> {
+
+    /**
+     * 递增
+     *
+     * @return 递增后的ID
+     */
+    ID incr();
+
+    /**
+     * 递增
+     *
+     * @return 递增后的ID
+     */
+    ID incr(String key);
+}

+ 39 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageIdIncrementingServiceImpl.java

@@ -0,0 +1,39 @@
+package cn.sikey.websocket.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisOperations;
+import org.springframework.stereotype.Component;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 使用 Redis 构建的消息ID递增适配器
+ */
+@Slf4j
+@Component
+public class MessageIdIncrementingServiceImpl implements MessageIdIncrementingService<Long> {
+    /**
+     * Redis incr 的 key <br/>
+     * 消息ID通过 Redis 生成
+     */
+    private static final String MESSAGE_ID_INCREMENTING_KEY = "message.mcdisk.id";
+
+    private final RedisOperations<String, String> redisOperations;
+
+    @Autowired
+    public MessageIdIncrementingServiceImpl(RedisOperations<String, String> redisOperations) {
+        this.redisOperations = redisOperations;
+    }
+
+    @Override
+    public Long incr() {
+        return redisOperations.opsForValue().increment(MESSAGE_ID_INCREMENTING_KEY);
+    }
+
+    @Override
+    public Long incr(String key) {
+        return redisOperations.opsForValue().increment(key);
+    }
+
+}

+ 14 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageRecvService.java

@@ -0,0 +1,14 @@
+package cn.sikey.websocket.service;
+
+import cn.sikey.websocket.controller.app.message.vo.SaveMessageRecvReqVO;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/2
+ * @Description: 消息发送接收
+ */
+public interface MessageRecvService {
+    void saveMessageRecv(SaveMessageRecvReqVO saveMessageRecvReqVO);
+
+    void updateStatusById(Integer status, Long ackId);
+}

+ 46 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageRecvServiceImpl.java

@@ -0,0 +1,46 @@
+package cn.sikey.websocket.service;
+
+import cn.sikey.websocket.controller.app.message.vo.SaveMessageRecvReqVO;
+import cn.sikey.websocket.convert.MessageConvert;
+import cn.sikey.websocket.dal.dataobject.MessageRecvDO;
+import cn.sikey.websocket.dal.mysql.MessageRecvMapper;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/2
+ * @Description: 消息发送接收
+ */
+@Service
+@Slf4j
+public class MessageRecvServiceImpl implements MessageRecvService {
+
+    @Resource
+    private MessageRecvMapper messageRecvMapper;
+
+    /**
+     * 保存消息发送接收
+     *
+     * @param saveMessageRecvReqVO 消息发送接收
+     * @return
+     */
+    @Override
+    public void saveMessageRecv(SaveMessageRecvReqVO saveMessageRecvReqVO) {
+        MessageRecvDO messageRecvDO = MessageConvert.INSTANCE.convertMessageRecvDO(saveMessageRecvReqVO);
+        messageRecvMapper.saveMessageRecv(messageRecvDO);
+    }
+
+    /**
+     * 更新状态
+     *
+     * @param status 状态
+     * @param ackId  ackId
+     * @return
+     */
+    @Override
+    public void updateStatusById(Integer status, Long ackId) {
+        messageRecvMapper.updateStatusById(status, ackId);
+    }
+}

+ 16 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageSendService.java

@@ -0,0 +1,16 @@
+package cn.sikey.websocket.service;
+
+import cn.sikey.websocket.controller.app.message.vo.SaveMessageSendReqVO;
+import cn.sikey.websocket.message.Message;
+import cn.sikey.websocket.message.MessageId;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/2
+ * @Description: 消息发送
+ */
+public interface MessageSendService {
+    void saveMessageSend(Message message);
+
+    SaveMessageSendReqVO findMessageSendById(MessageId messageId);
+}

+ 50 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessageSendServiceImpl.java

@@ -0,0 +1,50 @@
+package cn.sikey.websocket.service;
+
+import cn.sikey.websocket.controller.app.message.vo.SaveMessageSendReqVO;
+import cn.sikey.websocket.convert.MessageConvert;
+import cn.sikey.websocket.dal.dataobject.MessageSendDO;
+import cn.sikey.websocket.dal.mysql.MessageSendMapper;
+import cn.sikey.websocket.message.Message;
+import cn.sikey.websocket.message.MessageId;
+import cn.sikey.websocket.util.ObjectMapperUtil;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/2
+ * @Description: 消息发送
+ */
+@Service
+@Slf4j
+public class MessageSendServiceImpl implements MessageSendService {
+
+    @Resource
+    private MessageSendMapper messageSendMapper;
+
+    /**
+     * 保存消息发送
+     *
+     * @param message 消息
+     * @return
+     */
+    @Override
+    public void saveMessageSend(Message message) {
+        SaveMessageSendReqVO saveMessageSendReqVO = new SaveMessageSendReqVO();
+        saveMessageSendReqVO.setId(message.getId().getId());
+        saveMessageSendReqVO.setMsgType(message.getType().getValue());
+        saveMessageSendReqVO.setSenderId(message.getSenderId());
+        saveMessageSendReqVO.setSendTime(message.getSendTime());
+        saveMessageSendReqVO.setContent(ObjectMapperUtil.convertValue(message.getContent(), ObjectMapperUtil.getValueTypeRef()));
+
+        MessageSendDO messageSendDO = MessageConvert.INSTANCE.convertMessageSendDO(saveMessageSendReqVO);
+        messageSendMapper.saveMessageSend(messageSendDO);
+    }
+
+    @Override
+    public SaveMessageSendReqVO findMessageSendById(MessageId messageId) {
+        MessageSendDO messageSendDO = messageSendMapper.findById(messageId);
+        return MessageConvert.INSTANCE.convertSaveMessageSendReqVO(messageSendDO);
+    }
+}

+ 13 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessagingPublishService.java

@@ -0,0 +1,13 @@
+package cn.sikey.websocket.service;
+
+import cn.sikey.websocket.controller.app.message.vo.MessagingPublishReqVO;
+import cn.sikey.websocket.controller.app.message.vo.MessagingPublishRespVO;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/2
+ * @Description: 发消息
+ */
+public interface MessagingPublishService {
+    <T> MessagingPublishRespVO publish(MessagingPublishReqVO<T> request);
+}

+ 15 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessagingPublishServiceImpl.java

@@ -0,0 +1,15 @@
+package cn.sikey.websocket.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/2
+ * @Description: 发消息
+ */
+@Service
+@Slf4j
+public class MessagingPublishServiceImpl extends AbstractWebSocketService implements MessagingPublishService {
+
+}

+ 12 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessagingSubscribeService.java

@@ -0,0 +1,12 @@
+package cn.sikey.websocket.service;
+
+import cn.sikey.websocket.message.Message;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/2
+ * @Description: 订阅消息
+ */
+public interface MessagingSubscribeService {
+    void consumeMessage(Message message);
+}

+ 15 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/service/MessagingSubscribeServiceImpl.java

@@ -0,0 +1,15 @@
+package cn.sikey.websocket.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/7/2
+ * @Description: 订阅消息
+ */
+@Slf4j
+@Service
+public class MessagingSubscribeServiceImpl extends AbstractWebSocketService implements MessagingSubscribeService {
+
+}

+ 47 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/util/ConnectionConverterUtil.java

@@ -0,0 +1,47 @@
+package cn.sikey.websocket.util;
+
+import cn.hutool.core.util.CharsetUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpUtil;
+import cn.sikey.framework.common.exception.ServiceException;
+import cn.sikey.websocket.websocket.Connection;
+import lombok.experimental.UtilityClass;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.time.Instant;
+import java.util.Map;
+import java.util.Objects;
+
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 连接转换器
+ */
+@UtilityClass
+public class ConnectionConverterUtil {
+
+    /**
+     * 转换
+     *
+     * @param session WebSocketSession
+     * @return Connection
+     */
+    public Connection to(WebSocketSession session) {
+        Objects.requireNonNull(session.getUri());
+        Map<String, String> request = HttpUtil.decodeParamMap(session.getUri().getQuery(), CharsetUtil.CHARSET_UTF_8);
+        String id = request.get("id");
+
+        if (StrUtil.isBlank(id)) {
+            throw new ServiceException(HttpStatus.INTERNAL_SERVER_ERROR.value(), "id is required");
+        }
+
+
+        String webSocketKey = session.getHandshakeHeaders().get("Sec-WebSocket-Key").getFirst();
+        return new Connection()
+                .setId(id)
+                .setWebsocketKey(webSocketKey)
+                .setLastHeartbeat(Instant.now());
+    }
+}

+ 52 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/util/MessageConverterUtil.java

@@ -0,0 +1,52 @@
+package cn.sikey.websocket.util;
+
+import cn.sikey.websocket.message.Message;
+import cn.sikey.websocket.message.TestTextMessage;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import lombok.experimental.UtilityClass;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: MessageConverterUtil
+ */
+@UtilityClass
+public class MessageConverterUtil {
+    private static final ObjectMapper objectMapper;
+
+    static {
+        objectMapper = new ObjectMapper();
+        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 空字段也可以序列化
+        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);  // 空对象也可以序列化
+    }
+
+
+    /**
+     * 转换成 mq 的消息
+     *
+     * @param message 消息
+     * @return mq 消息
+     */
+    public Object toMessaging(Message message) {
+        return switch (message.getType()) {
+            case HEARTBEAT_MESSAGES, ACK_MESSAGES -> null;
+            case TEST_TEXT_MESSAGE -> convertChatTextMessage(message);
+        };
+    }
+
+    private TestTextMessage convertChatTextMessage(Message message) {
+        TestTextMessage messaging = new TestTextMessage();
+        messaging.setMsgId(message.getId().getId())
+                .setMsgType(message.getType())
+                .setRecvId(message.getReceiverId().getFirst())
+                .setSenderId(message.getSenderId())
+                .setSendTime(message.getSendTime());
+
+        messaging.setContent(
+                objectMapper.convertValue(message.getContent(), TestTextMessage.TestTextMessageContent.class));
+        return messaging;
+    }
+
+}

+ 44 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/util/ObjectMapperUtil.java

@@ -0,0 +1,44 @@
+package cn.sikey.websocket.util;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.experimental.UtilityClass;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: ObjectMapperUtil
+ */
+@UtilityClass
+public class ObjectMapperUtil {
+    private final static ObjectMapper objectMapper;
+
+    static {
+        objectMapper = new Jackson2ObjectMapperBuilder()
+                .createXmlMapper(false)
+                .failOnEmptyBeans(false)
+                .failOnUnknownProperties(false)
+                .build();
+//        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
+        objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
+    }
+
+    public <T> T convertValue(Object fromValue, Class<T> toValueType) {
+        return objectMapper.convertValue(fromValue, toValueType);
+    }
+
+    public String writeValueAsString(Object value) throws Exception {
+        return objectMapper.writeValueAsString(value);
+    }
+
+    public <T> T convertValue(Object fromValue, TypeReference<T> toValueTypeRef) {
+        return objectMapper.convertValue(fromValue, toValueTypeRef);
+    }
+
+    public <T> TypeReference<T> getValueTypeRef() {
+        return new TypeReference<>() {
+        };
+    }
+}

+ 15 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/websocket/Ack.java

@@ -0,0 +1,15 @@
+package cn.sikey.websocket.websocket;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@ToString
+public class Ack {
+    private Long id;
+    private String receiverId;
+}

+ 44 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/websocket/Connection.java

@@ -0,0 +1,44 @@
+package cn.sikey.websocket.websocket;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.time.Duration;
+import java.time.Instant;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 连接
+ */
+@Data
+@Accessors(chain = true)
+@EqualsAndHashCode(callSuper = false)
+public class Connection {
+    /**
+     * 连接用户ID
+     */
+    private String id;
+
+    /**
+     * WebSocket Key
+     */
+    private String websocketKey;
+
+    /**
+     * 上次心跳时间
+     */
+    private Instant lastHeartbeat;
+
+
+    /**
+     * 是否已经死亡
+     *
+     * @param heartbeatTimeout 心跳超时时间
+     * @return 是否已经死亡
+     */
+    synchronized public boolean isDead(Duration heartbeatTimeout) {
+        return lastHeartbeat.plus(heartbeatTimeout).isBefore(Instant.now());
+    }
+}

+ 55 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/websocket/ConnectionHeartbeatService.java

@@ -0,0 +1,55 @@
+package cn.sikey.websocket.websocket;
+
+import cn.hutool.json.JSONUtil;
+import cn.sikey.websocket.config.CacheConnectionStateManager;
+import cn.sikey.websocket.util.ConnectionConverterUtil;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.web.socket.PongMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 心跳
+ */
+@Slf4j
+@Service
+public class ConnectionHeartbeatService {
+
+    @Resource
+    private CacheConnectionStateManager cacheConnectionStateManager;
+
+    /**
+     * 自定义消息处理
+     *
+     * @param session 会话
+     */
+    public void handleCustomizedMessage(WebSocketSession session) {
+        log.info("Refresh heartbeat after receiving message: {}", session);
+
+        Connection connection = ConnectionConverterUtil.to(session);
+        try {
+            cacheConnectionStateManager.heartbeat(connection);
+        } catch (Exception e) {
+            log.error("There was a problem processing the heartbeat message", e);
+            cacheConnectionStateManager.offline(connection);
+        }
+    }
+
+
+    /**
+     * 处理Pong消息
+     *
+     * @param session     会话
+     * @param pongMessage pongMessage
+     * @throws IOException 异常
+     */
+    public void handlePongMessage(WebSocketSession session, PongMessage pongMessage) throws IOException {
+        log.debug("Received PONG from {},pongMessage消息:{}", session.getId(), JSONUtil.toJsonPrettyStr(pongMessage));
+    }
+
+}

+ 69 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/java/cn/sikey/websocket/websocket/ConnectionManagerService.java

@@ -0,0 +1,69 @@
+package cn.sikey.websocket.websocket;
+
+import cn.sikey.websocket.config.CacheConnectionStateManager;
+import cn.sikey.websocket.config.WebSocketConfig;
+import cn.sikey.websocket.util.ConnectionConverterUtil;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Service;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.List;
+
+/**
+ * @Author: nelson
+ * @Date: 2025/5/29
+ * @Description: 连接管理
+ */
+@Slf4j
+@Service
+@RefreshScope
+public class ConnectionManagerService {
+
+    @Resource
+    private CacheConnectionStateManager cacheConnectionStateManager;
+
+    private WebSocketConfig webSocketConfig;
+
+    @Autowired
+    public void setWebSocketConfig(WebSocketConfig webSocketConfig) {
+        this.webSocketConfig = webSocketConfig;
+    }
+
+    /**
+     * 连接建立
+     */
+    public void connectionEstablished(WebSocketSession webSocketSession) throws Exception {
+        log.info("Connection established");
+
+        Connection connection = ConnectionConverterUtil.to(webSocketSession);
+        cacheConnectionStateManager.online(connection, webSocketSession);
+    }
+
+    /**
+     * 连接关闭
+     */
+    public void connectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) {
+        log.info("Connection closed code: {}", closeStatus.getCode());
+
+        Connection connection = ConnectionConverterUtil.to(webSocketSession);
+        cacheConnectionStateManager.offline(connection);
+    }
+
+    /**
+     * 移除死连接
+     */
+    public void removeDeadConnection() {
+        List<Connection> connections = cacheConnectionStateManager.getConnections();
+        log.debug("Remove dead connection. Current number of online connections: {}", connections.size());
+        for (Connection connection : connections) {
+            if (connection.isDead(webSocketConfig.getHeartbeatTimeout().plus(webSocketConfig.getCheckTimeout()))) {
+                log.info("Remove dead connection. Connection: {}", connection);
+                cacheConnectionStateManager.offline(connection);
+            }
+        }
+    }
+}

+ 155 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/resources/application.yml

@@ -0,0 +1,155 @@
+server:
+  port: 20003
+
+spring:
+  application:
+    name: sikey-websocket-business
+  profiles:
+    active: "test"
+  main:
+    allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
+    allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务
+  #  web-application-type: reactive
+  #config:
+    #import: "consul:"
+  # Jackson 配置项
+  jackson:
+    serialization:
+      write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳
+      write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401
+      write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
+      fail-on-empty-beans: false # 允许序列化无属性的 Bean
+  # Consul 集成配置
+  cloud:
+    consul:
+      host: 106.75.230.4
+      #host: 127.0.0.1
+      port: 8500
+      # discovery
+      discovery:
+        enabled: true
+        prefer-ip-address: true
+        instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}
+      # config
+      config:
+        enabled: false
+        format: YAML
+        data-key: data
+
+# MyBatis Plus 的配置项
+mybatis-plus:
+  configuration:
+    map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
+  global-config:
+    db-config:
+      id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。
+      #      id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库
+      #      id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
+      #      id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解
+      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
+      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
+    banner: false # 关闭控制台的 Banner 打印
+  type-aliases-package: ${sikey.info.base-package}.dal.dataobject
+
+--- #################### 数据库相关配置 ####################
+spring:
+  # 数据源配置项
+  autoconfigure:
+    exclude:
+  datasource:
+    druid: # Druid 【监控】相关的全局配置
+      web-stat-filter:
+        enabled: true
+      stat-view-servlet:
+        enabled: true
+        allow: # 设置白名单,不填则允许所有访问
+        url-pattern: /druid/*
+        login-username: # 控制台管理用户名和密码
+        login-password:
+      filter:
+        stat:
+          enabled: true
+          log-slow-sql: true # 慢 SQL 记录
+          slow-sql-millis: 100
+          merge-sql: true
+        wall:
+          config:
+            multi-statement-allow: true
+    dynamic: # 多数据源配置
+      druid: # Druid 【连接池】相关的全局配置
+        initial-size: 5 # 初始连接数
+        min-idle: 10 # 最小连接池数量
+        max-active: 20 # 最大连接池数量
+        max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
+        time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
+        min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
+        max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
+        validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
+        test-while-idle: true
+        test-on-borrow: true
+        test-on-return: true
+      primary: master
+      datasource:
+        master:
+          url: jdbc:mysql://106.75.230.4:13306/mc_disk?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10 # MySQL Connector/J 8.X 连接的示例
+          #url: jdbc:mysql://127.0.0.1:3306/mc_disk?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
+          username: root
+          password: 9RKdJsEQKnjrni9R
+          #password: 1qaz@WSX
+        slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
+          lazy: true # 开启懒加载,保证启动速度
+          url: jdbc:mysql://106.75.230.4:13306/mc_disk?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10 # MySQL Connector/J 8.X 连接的示例
+          #url: jdbc:mysql://127.0.0.1:3306/mc_disk?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
+          username: root
+          password: 9RKdJsEQKnjrni9R
+          #password: 1qaz@WSX
+---
+# Spring Data Redis 配置
+spring:
+  data:
+    redis:
+      repositories:
+        enabled: true
+      host: 106.75.230.4
+      port: 6379
+      password: sikey!Q@W#E456
+      timeout: 5000
+
+---
+# rabbitmq 配置
+spring:
+  rabbitmq:
+    host: 106.75.230.4
+    port: 5672
+    username: wa04
+    password: wa04@skey123
+    # 连接池配置
+    cache:
+      connection:
+        mode: CONNECTION   # 复用连接池
+        size: 10           # 最大活跃连接数
+      channel:
+        size: 100          # 单连接通道缓存数
+        checkout-timeout: 10000  # 获取通道超时时间(毫秒)
+    virtual-host: /
+    connection-timeout: 5000
+    requested-heartbeat: 60
+    publisher-confirm-type: correlated
+    messaging:
+      queue-name: publish_mcdisk_queue
+      exchange-name: mcdisk_exchange.topic
+      routing-key: publish.#
+    # 重试机制
+    listener:
+      simple:
+        retry:
+          enabled: true
+          max-attempts: 3
+          initial-interval: 1000
+          max-interval: 10000
+          multiplier: 2
+
+sikey:
+  info:
+    version: 1.0.0
+    base-package: cn.sikey.websocket

+ 76 - 0
sikey-websocket-business/sikey-websocket-business-biz/src/main/resources/logback-spring-test.xml

@@ -0,0 +1,76 @@
+<configuration>
+    <!-- 引用 Spring Boot 的 logback 基础配置 -->
+    <include resource="org/springframework/boot/logging/logback/defaults.xml" />
+    <!-- 变量 sikey.info.base-package,基础业务包 -->
+    <springProperty scope="context" name="sikey.info.base-package" source="sikey.info.base-package"/>
+    <!-- 格式化输出:%d 表示日期,%X{tid} SkWalking 链路追踪编号,%thread 表示线程名,%-5level:级别从左显示 5 个字符宽度,%msg:日志消息,%n是换行符 -->
+    <property name="PATTERN_DEFAULT" value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} | %highlight(${LOG_LEVEL_PATTERN:-%5p} ${PID:- }) | %boldYellow(%thread [%tid]) %boldGreen(%-40.40logger{39}) | %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
+
+    <!-- 控制台 Appender -->
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">     
+        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
+            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
+                <pattern>${PATTERN_DEFAULT}</pattern>
+            </layout>
+        </encoder>
+    </appender>
+
+    <!-- 文件 Appender -->
+    <!-- 参考 Spring Boot 的 file-appender.xml 编写 -->
+    <appender name="FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
+            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
+                <pattern>${PATTERN_DEFAULT}</pattern>
+            </layout>
+        </encoder>
+        <!-- 日志文件名 -->
+        <file>${LOG_FILE}</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <!-- 滚动后的日志文件名 -->
+            <fileNamePattern>${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}</fileNamePattern>
+            <!-- 启动服务时,是否清理历史日志,一般不建议清理 -->
+            <cleanHistoryOnStart>${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false}</cleanHistoryOnStart>
+            <!-- 日志文件,到达多少容量,进行滚动 -->
+            <maxFileSize>${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB}</maxFileSize>
+            <!-- 日志文件的总大小,0 表示不限制 -->
+            <totalSizeCap>${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0}</totalSizeCap>
+            <!-- 日志文件的保留天数 -->
+            <maxHistory>${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30}</maxHistory>
+        </rollingPolicy>
+    </appender>
+    <!-- 异步写入日志,提升性能 -->
+    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
+        <!-- 不丢失日志。默认的,如果队列的 80% 已满,则会丢弃 TRACT、DEBUG、INFO 级别的日志 -->
+        <discardingThreshold>0</discardingThreshold>
+        <!-- 更改默认的队列的深度,该值会影响性能。默认值为 256 -->
+        <queueSize>256</queueSize>
+        <appender-ref ref="FILE"/>
+    </appender>
+
+    <!-- SkyWalking GRPC 日志收集,实现日志中心。注意:SkyWalking 8.4.0 版本开始支持 -->
+    <appender name="GRPC" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
+        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
+            <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
+                <pattern>${PATTERN_DEFAULT}</pattern>
+            </layout>
+        </encoder>
+    </appender>
+
+    <!-- 本地环境 -->
+    <springProfile name="local">
+        <root level="INFO">
+            <appender-ref ref="STDOUT"/>
+            <appender-ref ref="GRPC"/> <!-- 本地环境下,如果不想接入 SkyWalking 日志服务,可以注释掉本行 -->
+            <appender-ref ref="ASYNC"/>  <!-- 本地环境下,如果不想打印日志,可以注释掉本行 -->
+        </root>
+    </springProfile>
+    <!-- 其它环境 -->
+    <springProfile name="dev,test,stage,prod,default">
+        <root level="INFO">
+            <appender-ref ref="STDOUT"/>
+            <appender-ref ref="ASYNC"/>
+            <appender-ref ref="GRPC"/>
+        </root>
+    </springProfile>
+
+</configuration>