ageer 1 年間 前
コミット
a079ef44e5
100 ファイル変更6841 行追加73 行削除
  1. BIN
      image/07.png
  2. BIN
      image/08.png
  3. BIN
      image/10.png
  4. BIN
      image/11.png
  5. 17 11
      pom.xml
  6. 9 4
      ruoyi-admin/pom.xml
  7. 1 1
      ruoyi-admin/src/main/java/com/xmzs/controller/CaptchaController.java
  8. 52 18
      ruoyi-admin/src/main/java/com/xmzs/controller/ChatController.java
  9. 3 1
      ruoyi-admin/src/main/java/com/xmzs/controller/PayController.java
  10. 51 0
      ruoyi-admin/src/main/java/com/xmzs/controller/WeChatController.java
  11. 1 1
      ruoyi-admin/src/main/resources/application-dev.yml
  12. 40 10
      ruoyi-admin/src/main/resources/application.yml
  13. 1 0
      ruoyi-common/pom.xml
  14. 7 0
      ruoyi-common/ruoyi-common-bom/pom.xml
  15. 0 7
      ruoyi-common/ruoyi-common-chat/pom.xml
  16. 5 3
      ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/constant/OpenAIConst.java
  17. 15 0
      ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/domain/request/MjTaskRequest.java
  18. 48 0
      ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/Tts/TextToSpeech.java
  19. 15 0
      ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/Tts/TtsFormat.java
  20. 23 0
      ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/Tts/TtsVoice.java
  21. 8 0
      ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/BaseChatCompletion.java
  22. 13 0
      ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiApi.java
  23. 0 1
      ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiClient.java
  24. 64 0
      ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiStreamClient.java
  25. 3 0
      ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/utils/TikTokensUtil.java
  26. 12 13
      ruoyi-common/ruoyi-common-pay/src/main/java/com/xmzs/common/config/PayConfig.java
  27. 0 1
      ruoyi-common/ruoyi-common-pay/src/main/java/com/xmzs/common/service/PayService.java
  28. 0 1
      ruoyi-common/ruoyi-common-pay/src/main/java/com/xmzs/common/service/impl/PayServiceImpl.java
  29. 3 1
      ruoyi-common/ruoyi-common-security/src/main/java/com/xmzs/common/security/handler/AllUrlHandler.java
  30. 51 0
      ruoyi-common/ruoyi-common-wechat/pom.xml
  31. 34 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/Wechat.java
  32. 43 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/api/AssistTools.java
  33. 424 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/api/MessageTools.java
  34. 215 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/api/WechatTools.java
  35. 37 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/beans/AppInfo.java
  36. 293 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/beans/BaseMsg.java
  37. 146 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/beans/RecommendInfo.java
  38. 90 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/controller/LoginController.java
  39. 276 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/core/Core.java
  40. 171 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/core/MsgCenter.java
  41. 91 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/face/IMsgHandlerFace.java
  42. 82 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/service/ILoginService.java
  43. 687 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/service/impl/LoginServiceImpl.java
  44. 38 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/thread/CheckLoginStatusThread.java
  45. 81 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/Config.java
  46. 34 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/ConstantConfigEnum.java
  47. 6 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/MsgKeywords.java
  48. 194 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/MyHttpClient.java
  49. 20 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/SleepUtils.java
  50. 68 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/MsgCodeEnum.java
  51. 38 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/MsgTypeEnum.java
  52. 13 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/OsNameEnum.java
  53. 35 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/ResultEnum.java
  54. 30 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/RetCodeEnum.java
  55. 59 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/StorageLoginInfoEnum.java
  56. 49 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/URLEnum.java
  57. 28 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/VerifyFriendEnum.java
  58. 36 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/parameters/BaseParaEnum.java
  59. 31 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/parameters/LoginParaEnum.java
  60. 30 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/parameters/StatusNotifyParaEnum.java
  61. 33 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/parameters/UUIDParaEnum.java
  62. 243 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/tools/CommonTools.java
  63. 80 0
      ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/tools/DownloadTools.java
  64. 2 0
      ruoyi-modules/pom.xml
  65. 50 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/pom.xml
  66. 32 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/constant/Constants.java
  67. 51 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/exception/BaseException.java
  68. 60 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/IBaseConnectionListener.java
  69. 149 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/IBaseMsgListener.java
  70. 46 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/IDanmuMsgListener.java
  71. 47 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/IEnterRoomMsgListener.java
  72. 47 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/IGiftMsgListener.java
  73. 46 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/ILikeMsgListener.java
  74. 47 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/ISuperChatMsgListener.java
  75. 33 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/BaseCmdMsg.java
  76. 74 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/BaseMsg.java
  77. 38 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/ICmdMsg.java
  78. 67 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/IDanmuMsg.java
  79. 62 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/IEnterRoomMsg.java
  80. 100 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/IGiftMsg.java
  81. 71 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/ILikeMsg.java
  82. 34 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/IMsg.java
  83. 49 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/ISuperChatMsg.java
  84. 54 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-client/pom.xml
  85. 200 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-client/src/main/java/tech/ordinaryroad/live/chat/client/commons/client/BaseLiveChatClient.java
  86. 135 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-client/src/main/java/tech/ordinaryroad/live/chat/client/commons/client/IBaseLiveChatClient.java
  87. 139 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-client/src/main/java/tech/ordinaryroad/live/chat/client/commons/client/config/BaseLiveChatClientConfig.java
  88. 83 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-client/src/main/java/tech/ordinaryroad/live/chat/client/commons/client/enums/ClientStatusEnums.java
  89. 24 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-util/pom.xml
  90. 80 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-util/src/main/java/tech/ordinaryroad/live/chat/client/commons/util/OrLiveChatCookieUtil.java
  91. 40 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-util/src/main/java/tech/ordinaryroad/live/chat/client/commons/util/OrLiveChatNumberUtil.java
  92. 50 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-util/src/main/java/tech/ordinaryroad/live/chat/client/commons/util/OrLiveChatReflectUtil.java
  93. 54 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-util/src/main/java/tech/ordinaryroad/live/chat/client/commons/util/OrLocalDateTimeUtil.java
  94. 43 0
      ruoyi-modules/ruoyi-live/live-chat-client-commons/pom.xml
  95. 54 0
      ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty-client/pom.xml
  96. 349 0
      ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty-client/src/main/java/tech/ordinaryroad/live/chat/client/servers/netty/client/base/BaseNettyClient.java
  97. 65 0
      ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty-client/src/main/java/tech/ordinaryroad/live/chat/client/servers/netty/client/config/BaseNettyClientConfig.java
  98. 66 0
      ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty-client/src/main/java/tech/ordinaryroad/live/chat/client/servers/netty/client/handler/BaseNettyClientBinaryFrameHandler.java
  99. 65 0
      ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty-client/src/main/java/tech/ordinaryroad/live/chat/client/servers/netty/client/handler/BaseNettyClientConnectionHandler.java
  100. 58 0
      ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty/pom.xml

BIN
image/07.png


BIN
image/08.png


BIN
image/10.png


BIN
image/11.png


+ 17 - 11
pom.xml

@@ -10,7 +10,7 @@
 
     <name>ruoyi-ai</name>
     <url>https://gitee.com/ageerle/ruoyi-ai</url>
-    <description>AI助手后台管理系统</description>
+    <description>AI助手</description>
 
     <properties>
         <revision>1.0.0</revision>
@@ -242,16 +242,16 @@
                 <version>${tencent.sms.version}</version>
             </dependency>
 
-            <dependency>
-                <groupId>de.codecentric</groupId>
-                <artifactId>spring-boot-admin-starter-server</artifactId>
-                <version>${spring-boot-admin.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>de.codecentric</groupId>
-                <artifactId>spring-boot-admin-starter-client</artifactId>
-                <version>${spring-boot-admin.version}</version>
-            </dependency>
+<!--            <dependency>-->
+<!--                <groupId>de.codecentric</groupId>-->
+<!--                <artifactId>spring-boot-admin-starter-server</artifactId>-->
+<!--                <version>${spring-boot-admin.version}</version>-->
+<!--            </dependency>-->
+<!--            <dependency>-->
+<!--                <groupId>de.codecentric</groupId>-->
+<!--                <artifactId>spring-boot-admin-starter-client</artifactId>-->
+<!--                <version>${spring-boot-admin.version}</version>-->
+<!--            </dependency>-->
 
             <!--redisson-->
             <dependency>
@@ -318,6 +318,12 @@
                 <version>${revision}</version>
             </dependency>
 
+            <dependency>
+                <groupId>com.xmzs</groupId>
+                <artifactId>ruoyi-midjourney</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
             <dependency>
                 <groupId>com.xmzs</groupId>
                 <artifactId>ruoyi-generator</artifactId>

+ 9 - 4
ruoyi-admin/pom.xml

@@ -59,6 +59,11 @@
             <artifactId>ruoyi-job</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.xmzs</groupId>
+            <artifactId>ruoyi-midjourney</artifactId>
+        </dependency>
+
         <!-- 代码生成-->
         <dependency>
             <groupId>com.xmzs</groupId>
@@ -71,10 +76,10 @@
             <artifactId>ruoyi-demo</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>de.codecentric</groupId>
-            <artifactId>spring-boot-admin-starter-client</artifactId>
-        </dependency>
+<!--        <dependency>-->
+<!--            <groupId>de.codecentric</groupId>-->
+<!--            <artifactId>spring-boot-admin-starter-client</artifactId>-->
+<!--        </dependency>-->
 
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 1 - 1
ruoyi-admin/src/main/java/com/xmzs/controller/CaptchaController.java

@@ -94,7 +94,7 @@ public class CaptchaController {
         String code = RandomUtil.randomNumbers(4);
         RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
         try {
-            MailUtils.sendText(emailRequest.getUsername(), "【GPT助手】登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
+            MailUtils.sendText(emailRequest.getUsername(), "【熊猫助手】登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。");
         } catch (Exception e) {
             log.error("验证码短信发送异常 => {}", e.getMessage());
             return R.fail(e.getMessage());

+ 52 - 18
ruoyi-admin/src/main/java/com/xmzs/controller/ChatController.java

@@ -3,7 +3,10 @@ package com.xmzs.controller;
 
 import com.xmzs.common.chat.domain.request.ChatRequest;
 import com.xmzs.common.chat.domain.request.Dall3Request;
+import com.xmzs.common.chat.domain.request.MjTaskRequest;
+import com.xmzs.common.chat.entity.Tts.TextToSpeech;
 import com.xmzs.common.chat.entity.images.Item;
+import com.xmzs.common.chat.entity.whisper.WhisperResponse;
 import com.xmzs.common.core.domain.R;
 import com.xmzs.common.core.domain.model.LoginUser;
 import com.xmzs.common.core.exception.base.BaseException;
@@ -13,19 +16,30 @@ import com.xmzs.common.satoken.utils.LoginHelper;
 import com.xmzs.system.domain.bo.ChatMessageBo;
 import com.xmzs.system.domain.vo.ChatMessageVo;
 import com.xmzs.system.service.IChatMessageService;
-import com.xmzs.system.service.SseService;
+import com.xmzs.system.service.ISseService;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
+import org.springframework.core.io.Resource;
+
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.List;
 
+import retrofit2.Response;
+
 /**
  * 描述:
  *
@@ -37,38 +51,58 @@ import java.util.List;
 @RequiredArgsConstructor
 public class ChatController {
 
-    private final SseService sseService;
+    private final ISseService ISseService;
 
-    private final  IChatMessageService chatMessageService;
+    private final IChatMessageService chatMessageService;
 
     /**
      * 聊天接口
      */
     @PostMapping("/chat")
     @ResponseBody
-    public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest) {
-        if("gpt-4-all".equals(chatRequest.getModel())
-            || chatRequest.getModel().startsWith("gpt-4-gizmo")
-            || chatRequest.getModel().startsWith("net-")
-        ){
-            return sseService.transitChat(chatRequest);
-        }
-        if("azure-gpt-3.5".equals(chatRequest.getModel())){
-            return sseService.azureChat(chatRequest);
-        }
-        return sseService.sseChat(chatRequest);
+    public SseEmitter sseChat(@RequestBody @Valid ChatRequest chatRequest, HttpServletResponse response) {
+        return ISseService.sseChat(chatRequest);
+    }
+
+    /**
+     * 语音转文本
+     *
+     * @param file
+     */
+    @PostMapping("/audio")
+    @ResponseBody
+    public WhisperResponse audio(@RequestParam("file") MultipartFile file) {
+        WhisperResponse whisperResponse = ISseService.speechToTextTranscriptionsV2(file);
+        return whisperResponse;
     }
 
+    /**
+     * 文本转语音
+     *
+     * @param textToSpeech
+     */
+    @PostMapping("/speech")
+    @ResponseBody
+    public ResponseEntity<Resource> speech(@RequestBody TextToSpeech textToSpeech) {
+        return ISseService.textToSpeed(textToSpeech);
+    }
+
+
     @PostMapping("/dall3")
     @ResponseBody
     public R<List<Item>> dall3(@RequestBody @Valid Dall3Request request) {
-       return R.ok(sseService.dall3(request));
+        return R.ok(ISseService.dall3(request));
     }
 
+    /**
+     * 扣除mj绘图费用
+     *
+     * @return
+     */
     @PostMapping("/mjTask")
     @ResponseBody
-    public R<String> mjTask() {
-        sseService.mjTask();
+    public R<String> mjTask(@RequestBody MjTaskRequest mjTaskRequest) {
+        ISseService.mjTask(mjTaskRequest);
         return R.ok();
     }
 
@@ -77,7 +111,7 @@ public class ChatController {
      */
     @PostMapping("/chatList")
     @ResponseBody
-    public R<TableDataInfo<ChatMessageVo>> list(@RequestBody @Valid ChatMessageBo chatRequest,@RequestBody PageQuery pageQuery) {
+    public R<TableDataInfo<ChatMessageVo>> list(@RequestBody @Valid ChatMessageBo chatRequest, @RequestBody PageQuery pageQuery) {
         // 默认查询当前登录用户消息记录
         LoginUser loginUser = LoginHelper.getLoginUser();
         if (loginUser == null) {

+ 3 - 1
ruoyi-admin/src/main/java/com/xmzs/controller/PayController.java

@@ -73,7 +73,6 @@ public class PayController {
         return R.ok(paymentOrdersVo);
     }
 
-
     /**
      * 跳转通知地址
      *
@@ -138,6 +137,9 @@ public class PayController {
         BeanUtil.copyProperties(paymentOrdersVo,paymentOrdersBo);
         paymentOrdersService.updateByBo(paymentOrdersBo);
         SysUserVo sysUserVo = userService.selectUserById(paymentOrdersVo.getUserId());
+        if(money>9.9){
+            money = money*2;
+        }
         sysUserVo.setUserBalance(sysUserVo.getUserBalance()+money);
         SysUserBo sysUserBo = new SysUserBo();
         BeanUtil.copyProperties(sysUserVo,sysUserBo);

+ 51 - 0
ruoyi-admin/src/main/java/com/xmzs/controller/WeChatController.java

@@ -0,0 +1,51 @@
+package com.xmzs.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import com.xmzs.common.wechat.Wechat;
+import com.xmzs.system.cofing.KeywordConfig;
+import com.xmzs.system.cofing.QqConfig;
+import com.xmzs.system.cofing.WechatConfig;
+import com.xmzs.system.handler.WechatMessageHandler;
+import com.xmzs.system.service.ISseService;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 个人微信扩展控制器
+ *
+ * @author WangLe
+ */
+@SaIgnore
+@Slf4j
+@Validated
+@RequiredArgsConstructor
+@RestController
+public class WeChatController {
+
+    @Getter
+    private Wechat wechatBot;
+
+    private final WechatConfig wechatConfig;
+
+    private final ISseService sseService;
+
+    private final  KeywordConfig keywordConfig;
+
+    /**
+     * 获取微信登录二维码
+     *
+     */
+    @PostMapping("/getQr")
+    public void getQr() {
+        //微信
+        if (wechatConfig.getEnable()){
+            log.info("正在登录微信,请按提示操作:");
+            wechatBot = new Wechat(new WechatMessageHandler(sseService, keywordConfig), wechatConfig.getQrPath());
+            wechatBot.start();
+        }
+    }
+}

+ 1 - 1
ruoyi-admin/src/main/resources/application-dev.yml

@@ -51,7 +51,7 @@ spring:
           # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
           url: jdbc:mysql://127.0.0.1:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true
           username: ry-vue
-          password: ry-vue
+          password: xxx
 
         # 从库数据源
 #        slave:

+ 40 - 10
ruoyi-admin/src/main/resources/application.yml

@@ -220,11 +220,11 @@ mail:
   # 是否需要用户名密码验证
   auth: true
   # 发送方,遵循RFC-822标准
-  from: xxx@163.com
+  from: ageerle@163.com
   # 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
-  user: xxx@163.com
+  user: ageerle@163.com
   # 密码(填写授权码)
-  pass: pass
+  pass: TOGXBVPYFVPFRQMQ
   # 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
   starttlsEnable: true
   # 使用SSL安全连接
@@ -318,14 +318,10 @@ websocket:
   path: ''
   # 设置访问源地址
   allowedOrigins: '*'
-# AI助手配置信息
+# chatgpt配置信息
 chat:
-  apiKey: ''
-  apiHost: ''
-# 中转接口
-transit:
-  apiKey: ''
-  apiHost: 'https://api.gptgod.online/'
+  apiKey: 'sk-uMCP3lTg1dQ9L7Xs2bF352Fa216a4c9280577b205dE67e12'
+  apiHost: 'https://api.pandarobot.chat/'
 # 微信小程序配置信息
 wx:
   miniapp:
@@ -343,3 +339,37 @@ baidu:
     apiKey: '' # apiKey
     secretKey: '' # secretKey
 
+wechat:
+  #  是否使用微信 true/false
+  enable: true
+  #  生成的登录二维码路径 默认与项目同级
+  qrPath: "./"
+
+keyword:
+  #  重置会话指令
+  reset: "重置会话"
+  #  ai画图指令(DALL·E模型 https://platform.openai.com/docs/models/dall-e)
+  #  generation 根据关键词生成图片(https://platform.openai.com/docs/guides/images/generations)
+  image: "ai画图"
+  #  ai语音指令(TTS模型 https://platform.openai.com/docs/api-reference/audio)
+  audio: "ai语音"
+mj:
+  api-secret:
+  task-store:
+    type: in_memory
+    timeout: 30d
+  translate-way: gpt
+#  proxy:
+#    host: 127.0.0.1
+#    port: 10809
+  ng-discord:
+    server: https://discord.pandarobot.chat/
+    cdn: https://app.pandarobot.chat/
+    wss: https://gateway.pandarobot.chat/
+  openai:
+    gpt-api-url: 'https://api.pandarobot.chat/'
+    gpt-api-key: 'sk-xxx'
+  accounts:
+    - guild-id: 'xxxxx'
+      channel-id: 'xxxxx'
+      user-token: 'xx.xx'

+ 1 - 0
ruoyi-common/pom.xml

@@ -34,6 +34,7 @@
         <module>ruoyi-common-tenant</module>
         <module>ruoyi-common-chat</module>
         <module>ruoyi-common-pay</module>
+        <module>ruoyi-common-wechat</module>
     </modules>
 
     <artifactId>ruoyi-common</artifactId>

+ 7 - 0
ruoyi-common/ruoyi-common-bom/pom.xml

@@ -166,6 +166,13 @@
                 <version>${revision}</version>
             </dependency>
 
+            <!-- 微信模块 -->
+            <dependency>
+                <groupId>com.xmzs</groupId>
+                <artifactId>ruoyi-common-wechat</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
             <!-- 支付模块 -->
             <dependency>
                 <groupId>com.xmzs</groupId>

+ 0 - 7
ruoyi-common/ruoyi-common-chat/pom.xml

@@ -70,13 +70,6 @@
             <version>0.5.0</version>
         </dependency>
 
-        <!--   azure-ai    -->
-        <dependency>
-            <groupId>com.azure</groupId>
-            <artifactId>azure-ai-openai</artifactId>
-            <version>1.0.0-beta.6</version>
-        </dependency>
-
         <dependency>
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-all</artifactId>

+ 5 - 3
ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/constant/OpenAIConst.java

@@ -12,15 +12,17 @@ public class OpenAIConst {
 
     public final static int SUCCEED_CODE = 200;
 
-    public final static double GPT3_COST = 0.03;
+    public final static double GPT3_COST = 0.05;
 
     public final static double GPT4_COST = 0.3;
 
+    public final static double GPT4_ALL_COST = 0.3;
+
     /** 绘图费用 */
-    public final static double DALL3_COST = 0.3;
+    public final static double DALL3_COST = 0.4;
 
     /** 绘图费用-高清 */
-    public final static double DALL3_HD_COST = 0.6;
+    public final static double DALL3_HD_COST = 0.8;
 
     /** mdjourney绘图费用 */
     public final static double MJ_COST = 0.3;

+ 15 - 0
ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/domain/request/MjTaskRequest.java

@@ -0,0 +1,15 @@
+package com.xmzs.common.chat.domain.request;
+
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+
+/**
+ * mj任务请求实体类
+ *
+ * @author WangLe
+ */
+@Data
+public class MjTaskRequest {
+
+    private String prompt;
+}

+ 48 - 0
ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/Tts/TextToSpeech.java

@@ -0,0 +1,48 @@
+package com.xmzs.common.chat.entity.Tts;
+
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.*;
+
+@Data
+@Builder
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@NoArgsConstructor
+@AllArgsConstructor
+public class TextToSpeech {
+
+    @Builder.Default
+    private String model = Model.TTS_1.getName();
+    /**
+     * 音频声音源
+     *
+     * @see TtsVoice
+     */
+    private String voice;
+    /**
+     * 输入内容
+     */
+    private String input;
+    /**
+     * 输出音频文件格式
+     *
+     * @see TtsFormat
+     */
+    @JsonProperty("response_format")
+    private String responseFormat;
+    /**
+     * 速度调节,默认是1,取值范围0.25——4.0
+     */
+    private Double speed;
+
+
+    @Getter
+    @AllArgsConstructor
+    public enum Model {
+        TTS_1("tts-1"),
+        TTS_1_HD("tts-1-hd"),
+        ;
+        private final String name;
+    }
+}

+ 15 - 0
ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/Tts/TtsFormat.java

@@ -0,0 +1,15 @@
+package com.xmzs.common.chat.entity.Tts;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum TtsFormat {
+    MP3("mp3"),
+    OPUS("opus"),
+    AAC("aac"),
+    FLAC("flac"),
+    ;
+    private final String name;
+}

+ 23 - 0
ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/Tts/TtsVoice.java

@@ -0,0 +1,23 @@
+package com.xmzs.common.chat.entity.Tts;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 生成不同声音的音频
+ * <p>具体语音效果参考:https://platform.openai.com/docs/guides/text-to-speech</p>
+ */
+@Getter
+@AllArgsConstructor
+public enum TtsVoice {
+
+    ALLOY("alloy"),
+    ECHO("echo"),
+    FABLE("fable"),
+    ONYX("onyx"),
+    NOVA("nova"),
+    SHIMMER("shimmer"),
+    ;
+
+    private final String name;
+}

+ 8 - 0
ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/entity/chat/BaseChatCompletion.java

@@ -174,6 +174,10 @@ public class BaseChatCompletion implements Serializable {
          * gpt-3.5-turbo-16k-0613 超长上下文 支持函数
          */
         GPT_3_5_TURBO_16K_0613("gpt-3.5-turbo-16k-0613"),
+        /**
+         * gpt-3.5-turbo-0125 超长上下文 支持函数
+         */
+        GPT_3_5_TURBO_0125("gpt-3.5-turbo-0125"),
         /**
          * GPT4.0
          */
@@ -209,6 +213,10 @@ public class BaseChatCompletion implements Serializable {
          * 支持图片
          */
         GPT_4_VISION_PREVIEW("gpt-4-vision-preview"),
+        /**
+         * gpt-4-0613,支持函数
+         */
+        GPT_4_0125_PREVIEW("gpt-4-0125-preview"),
         ;
         private final String name;
     }

+ 13 - 0
ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiApi.java

@@ -1,5 +1,6 @@
 package com.xmzs.common.chat.openai;
 
+import com.xmzs.common.chat.entity.Tts.TextToSpeech;
 import com.xmzs.common.chat.entity.chat.ChatCompletionWithPicture;
 import io.reactivex.Single;
 import okhttp3.MultipartBody;
@@ -33,6 +34,7 @@ import com.xmzs.common.chat.entity.models.ModelResponse;
 import com.xmzs.common.chat.entity.moderations.Moderation;
 import com.xmzs.common.chat.entity.moderations.ModerationResponse;
 import com.xmzs.common.chat.entity.whisper.WhisperResponse;
+import retrofit2.Call;
 import retrofit2.http.*;
 
 import java.time.LocalDate;
@@ -340,4 +342,15 @@ public interface OpenAiApi {
      */
     @POST("v1/chat/completions")
     Single<ChatCompletionResponse> chatCompletionWithPicture(@Body ChatCompletionWithPicture chatCompletion);
+
+    /**
+     * 文本转语音
+     *
+     * @param textToSpeech 参数
+     * @return ResponseBody body
+     * @since 1.1.2
+     */
+    @POST("v1/audio/speech")
+    @Streaming
+    Call<ResponseBody> textToSpeech(@Body TextToSpeech textToSpeech);
 }

+ 0 - 1
ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiClient.java

@@ -668,7 +668,6 @@ public class OpenAiClient {
         return this.chatCompletion(chatCompletion);
     }
 
-
     /**
      * 语音翻译:目前仅支持翻译为英文
      *

+ 64 - 0
ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/openai/OpenAiStreamClient.java

@@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil;
 import cn.hutool.http.ContentType;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.xmzs.common.chat.config.LocalCache;
+import com.xmzs.common.chat.entity.Tts.TextToSpeech;
 import com.xmzs.common.chat.entity.billing.BillingUsage;
 import com.xmzs.common.chat.entity.billing.KeyInfo;
 import com.xmzs.common.chat.entity.billing.Subscription;
@@ -37,10 +38,12 @@ import com.xmzs.common.chat.entity.chat.ChatCompletion;
 import org.jetbrains.annotations.NotNull;
 import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import retrofit2.Call;
 import retrofit2.Retrofit;
 import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
 import retrofit2.converter.jackson.JacksonConverterFactory;
 
+import java.io.IOException;
 import java.io.InputStream;
 import java.net.http.HttpClient;
 import java.net.http.HttpRequest;
@@ -344,6 +347,67 @@ public class OpenAiStreamClient {
         Transcriptions transcriptions = Transcriptions.builder().build();
         return this.speechToTextTranscriptions(file, transcriptions);
     }
+    /**
+     * 文本转语音(异步)
+     *
+     * @param textToSpeech 参数
+     * @param callback     返回值接收
+     * @since 1.1.2
+     */
+    public void textToSpeech(TextToSpeech textToSpeech, retrofit2.Callback callback) {
+        retrofit2.Call<ResponseBody> responseBody = this.openAiApi.textToSpeech(textToSpeech);
+        responseBody.enqueue(callback);
+    }
+
+    /**
+     * 文本转语音(同步)
+     *
+     * @param textToSpeech 参数
+     * @since 1.1.3
+     */
+    public ResponseBody textToSpeech(TextToSpeech textToSpeech){
+        Call<ResponseBody> responseBody = this.openAiApi.textToSpeech(textToSpeech);
+        try {
+            return responseBody.execute().body();
+        } catch (IOException e) {
+            throw new BaseException("文本转语音(同步)失败: "+e.getMessage());
+        }
+    }
+
+    /**
+     * 文本转语音(克隆)
+     *
+     * @param textToSpeech
+     * @return
+     */
+    public ResponseBody textToSpeechClone(TextToSpeech textToSpeech) {
+        String baseUrl = "http://localhost:8081";
+        String spk = "三月七";
+        String text = textToSpeech.getInput();
+        String lang = "zh";
+
+        // 创建OkHttpClient实例
+        OkHttpClient client = new OkHttpClient();
+
+        // 构建请求URL
+        HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl).newBuilder();
+        urlBuilder.addQueryParameter("spk", spk);
+        urlBuilder.addQueryParameter("text", text);
+        urlBuilder.addQueryParameter("lang", lang);
+        String url = urlBuilder.build().toString();
+
+        // 创建请求对象
+        Request request = new Request.Builder()
+            .url(url)
+            .build();
+        // 发送请求并处理响应
+        try {
+            return client.newCall(request).execute().body();
+        } catch (IOException e) {
+            throw new BaseException("语音克隆失败!{}",e.getMessage());
+        }
+    }
+
 
     /**
      * 构造

+ 3 - 0
ruoyi-common/ruoyi-common-chat/src/main/java/com/xmzs/common/chat/utils/TikTokensUtil.java

@@ -6,6 +6,7 @@ import com.knuddels.jtokkit.api.Encoding;
 import com.knuddels.jtokkit.api.EncodingRegistry;
 import com.knuddels.jtokkit.api.EncodingType;
 import com.knuddels.jtokkit.api.ModelType;
+import com.xmzs.common.chat.entity.chat.BaseChatCompletion;
 import lombok.extern.slf4j.Slf4j;
 
 import com.xmzs.common.chat.entity.chat.ChatCompletion;
@@ -39,11 +40,13 @@ public class TikTokensUtil {
         modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_0613.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
         modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_16K.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
         modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_16K_0613.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
+        modelMap.put(ChatCompletion.Model.GPT_3_5_TURBO_0125.getName(), registry.getEncodingForModel(ModelType.GPT_3_5_TURBO));
         modelMap.put(ChatCompletion.Model.GPT_4_32K.getName(), registry.getEncodingForModel(ModelType.GPT_4));
         modelMap.put(ChatCompletion.Model.GPT_4_0613.getName(), registry.getEncodingForModel(ModelType.GPT_4));
         modelMap.put(ChatCompletion.Model.GPT_4_32K_0613.getName(), registry.getEncodingForModel(ModelType.GPT_4));
         modelMap.put(ChatCompletion.Model.GPT_4_1106_PREVIEW.getName(), registry.getEncodingForModel(ModelType.GPT_4));
         modelMap.put(ChatCompletion.Model.GPT_4_VISION_PREVIEW.getName(), registry.getEncodingForModel(ModelType.GPT_4));
+        modelMap.put(ChatCompletion.Model.GPT_4_0125_PREVIEW.getName(), registry.getEncodingForModel(ModelType.GPT_4));
     }
 
     /**

+ 12 - 13
ruoyi-common/ruoyi-common-pay/src/main/java/com/xmzs/common/config/PayConfig.java

@@ -10,27 +10,32 @@ public class PayConfig {
     /**
      * 商户ID
      */
-    public static String pid = "xx";
+    public static String pid = "xxx";
 
     /**
-     * 支付方式
+     * 接口地址
      */
-    public static String type = "wxpay";
+    public static String payUrl = "https://pay-cloud.vip/mapi.php";
 
     /**
-     * 接口地址
+     * 私钥
      */
-    public static String payUrl = "https://pay.bluetuo.com/mapi.php";
+    public static String key = "xxx";
 
     /**
      * 服务器异步通知地址
      */
-    public static String notify_url = "http://xx/pay/returnUrl";
+    public static String notify_url = "https://www.pandarobot.chat/pay/returnUrl";
 
     /**
      * 页面跳转通知地址
      */
-    public static String return_url = "http://xx/pay/notifyUrl";
+    public static String return_url = "https://www.pandarobot.chat/pay/notifyUrl";
+
+    /**
+     * 支付方式
+     */
+    public static String type = "wxpay";
 
     /**
      * 设备类型
@@ -40,12 +45,6 @@ public class PayConfig {
     /**
      * 加密方式默认MD5
      */
-
     public static String sign_type = "MD5";
 
-    /**
-     * 私钥
-     */
-    public static String key = "xx";
-
 }

+ 0 - 1
ruoyi-common/ruoyi-common-pay/src/main/java/com/xmzs/common/service/PayService.java

@@ -19,5 +19,4 @@ public interface PayService {
      * @return String
      **/
     String getPayUrl(String orderNo, String name, double money, String clientIp);
-
 }

+ 0 - 1
ruoyi-common/ruoyi-common-pay/src/main/java/com/xmzs/common/service/impl/PayServiceImpl.java

@@ -40,5 +40,4 @@ public class PayServiceImpl implements PayService {
         JSONObject jsonObject = new JSONObject(body);
         return (String) jsonObject.get("qrcode");
     }
-
 }

+ 3 - 1
ruoyi-common/ruoyi-common-security/src/main/java/com/xmzs/common/security/handler/AllUrlHandler.java

@@ -30,8 +30,10 @@ public class AllUrlHandler implements InitializingBean {
         Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
         map.keySet().forEach(info -> {
             // 获取注解上边的 path 替代 path variable 为 *
-            Objects.requireNonNull(info.getPathPatternsCondition().getPatterns())
+            if(info.getPathPatternsCondition()!=null){
+                Objects.requireNonNull(info.getPathPatternsCondition().getPatterns())
                     .forEach(url -> set.add(ReUtil.replaceAll(url.getPatternString(), PATTERN, "*")));
+            }
         });
         urls.addAll(set);
     }

+ 51 - 0
ruoyi-common/ruoyi-common-wechat/pom.xml

@@ -0,0 +1,51 @@
+<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/maven-v4_0_0.xsd">
+    <parent>
+        <groupId>com.xmzs</groupId>
+        <artifactId>ruoyi-common</artifactId>
+        <version>${revision}</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>ruoyi-common-wechat</artifactId>
+
+    <description>
+        ruoyi-common-wechat 微信服务
+    </description>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <!-- emoji -->
+        <dependency>
+            <groupId>com.vdurmont</groupId>
+            <artifactId>emoji-java</artifactId>
+            <version>3.2.0</version>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/javax.activation/activation -->
+        <dependency>
+            <groupId>javax.activation</groupId>
+            <artifactId>activation</artifactId>
+            <version>1.1.1</version>
+        </dependency>
+
+        <!--        qq          -->
+        <dependency>
+            <groupId>net.mamoe</groupId>
+            <artifactId>mirai-core-jvm</artifactId>
+            <version>2.16.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.xmzs</groupId>
+            <artifactId>ruoyi-common-json</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 34 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/Wechat.java

@@ -0,0 +1,34 @@
+package com.xmzs.common.wechat;
+
+import com.xmzs.common.wechat.controller.LoginController;
+import com.xmzs.common.wechat.core.MsgCenter;
+import com.xmzs.common.wechat.face.IMsgHandlerFace;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+
+public class Wechat {
+	private static final Logger LOG = LoggerFactory.getLogger(Wechat.class);
+	private IMsgHandlerFace msgHandler;
+
+	public Wechat(IMsgHandlerFace msgHandler, String qrPath) {
+		System.setProperty("jsse.enableSNIExtension", "false"); // 防止SSL错误
+		this.msgHandler = msgHandler;
+
+		// 登陆
+		LoginController login = new LoginController();
+		login.login(qrPath);
+	}
+
+	public void start() {
+		LOG.info("+++++++++++++++++++开始消息处理+++++++++++++++++++++");
+		new Thread(new Runnable() {
+			@Override
+			public void run() {
+				MsgCenter.handleMsg(msgHandler);
+			}
+		}).start();
+	}
+
+}

+ 43 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/api/AssistTools.java

@@ -0,0 +1,43 @@
+package com.xmzs.common.wechat.api;
+
+import java.io.File;
+import java.io.IOException;
+
+import okhttp3.Call;
+import okhttp3.MediaType;
+import okhttp3.MultipartBody;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+/**
+ * 辅助工具类,该类暂时未用,请忽略
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年5月22日 下午10:34:46
+ * @version 1.0
+ *
+ */
+public class AssistTools {
+	private static OkHttpClient client = new OkHttpClient();
+	private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
+
+	public static boolean sendQrPicToServer(String username, String password, String uploadUrl, String localPath)
+			throws IOException {
+		File file = new File(localPath);
+		RequestBody requestBody = new MultipartBody.Builder().addFormDataPart("username", username)
+				.addFormDataPart("password", password)
+				.addFormDataPart("file", file.getName(), RequestBody.create(MEDIA_TYPE_PNG, file)).build();
+		Request request = new Request.Builder().url(uploadUrl).post(requestBody).build();
+		Call call = client.newCall(request);
+		try {
+			Response response = call.execute();
+			System.out.println(response.body().string());
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return true;
+	}
+
+}

+ 424 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/api/MessageTools.java

@@ -0,0 +1,424 @@
+package com.xmzs.common.wechat.api;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+import javax.activation.MimetypesFileTypeMap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.Consts;
+import org.apache.http.HttpEntity;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.HttpMultipartMode;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+
+import com.xmzs.common.wechat.beans.BaseMsg;
+import com.xmzs.common.wechat.beans.RecommendInfo;
+import com.xmzs.common.wechat.core.Core;
+import com.xmzs.common.wechat.utils.Config;
+import com.xmzs.common.wechat.utils.MyHttpClient;
+import com.xmzs.common.wechat.utils.enums.StorageLoginInfoEnum;
+import com.xmzs.common.wechat.utils.enums.URLEnum;
+import com.xmzs.common.wechat.utils.enums.VerifyFriendEnum;
+
+/**
+ * 消息处理类
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年4月23日 下午2:30:37
+ * @version 1.0
+ *
+ */
+public class MessageTools {
+	private static Logger LOG = LoggerFactory.getLogger(MessageTools.class);
+	private static Core core = Core.getInstance();
+	private static MyHttpClient myHttpClient = core.getMyHttpClient();
+
+	/**
+	 * 根据UserName发送文本消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月4日 下午11:17:38
+	 * @param msg
+	 * @param toUserName
+	 */
+	private static void sendMsg(String text, String toUserName) {
+		if (text == null) {
+			return;
+		}
+		LOG.info(String.format("发送消息 %s: %s", toUserName, text));
+		webWxSendMsg(1, text, toUserName);
+	}
+
+	/**
+	 * 根据ID发送文本消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月6日 上午11:45:51
+	 * @param text
+	 * @param id
+	 */
+	public static void sendMsgById(String text, String id) {
+		if (text == null) {
+			return;
+		}
+		sendMsg(text, id);
+	}
+
+	/**
+	 * 根据NickName发送文本消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月4日 下午11:17:38
+	 * @param text
+	 * @param nickName
+	 */
+	public static boolean sendMsgByNickName(String text, String nickName) {
+		if (nickName != null) {
+			String toUserName = WechatTools.getUserNameByNickName(nickName);
+			if (toUserName != null) {
+				webWxSendMsg(1, text, toUserName);
+				return true;
+			}
+		}
+		return false;
+
+	}
+
+	/**
+	 * 消息发送
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月23日 下午2:32:02
+	 * @param msgType
+	 * @param content
+	 * @param toUserName
+	 */
+	public static void webWxSendMsg(int msgType, String content, String toUserName) {
+		String url = String.format(URLEnum.WEB_WX_SEND_MSG.getUrl(), core.getLoginInfo().get("url"));
+		Map<String, Object> msgMap = new HashMap<String, Object>();
+		msgMap.put("Type", msgType);
+		msgMap.put("Content", content);
+		msgMap.put("FromUserName", core.getUserName());
+		msgMap.put("ToUserName", toUserName == null ? core.getUserName() : toUserName);
+		msgMap.put("LocalID", new Date().getTime() * 10);
+		msgMap.put("ClientMsgId", new Date().getTime() * 10);
+		Map<String, Object> paramMap = core.getParamMap();
+		paramMap.put("Msg", msgMap);
+		paramMap.put("Scene", 0);
+		try {
+			String paramStr = JSON.toJSONString(paramMap);
+			HttpEntity entity = myHttpClient.doPost(url, paramStr);
+			EntityUtils.toString(entity, Consts.UTF_8);
+		} catch (Exception e) {
+			LOG.error("webWxSendMsg", e);
+		}
+	}
+
+	/**
+	 * 上传多媒体文件到 微信服务器,目前应该支持3种类型: 1. pic 直接显示,包含图片,表情 2.video 3.doc 显示为文件,包含PDF等
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月7日 上午12:41:13
+	 * @param filePath
+	 * @return
+	 */
+	private static JSONObject webWxUploadMedia(String filePath) {
+		File file = new File(filePath);
+		if (!file.exists() && file.isFile()) {
+			LOG.info("file is not exist");
+			return null;
+		}
+		String url = String.format(URLEnum.WEB_WX_UPLOAD_MEDIA.getUrl(), core.getLoginInfo().get("fileUrl"));
+		String mimeType = new MimetypesFileTypeMap().getContentType(file);
+		String mediaType = "";
+		if (mimeType == null) {
+			mimeType = "text/plain";
+		} else {
+			mediaType = mimeType.split("/")[0].equals("image") ? "pic" : "doc";
+		}
+		String lastModifieDate = new SimpleDateFormat("yyyy MM dd HH:mm:ss").format(new Date());
+		long fileSize = file.length();
+		String passTicket = (String) core.getLoginInfo().get("pass_ticket");
+		String clientMediaId = new Date().getTime()
+				+ String.valueOf(new Random().nextLong()).substring(0, 4);
+		String webwxDataTicket = MyHttpClient.getCookie("webwx_data_ticket");
+		if (webwxDataTicket == null) {
+			LOG.error("get cookie webwx_data_ticket error");
+			return null;
+		}
+
+		Map<String, Object> paramMap = core.getParamMap();
+
+		paramMap.put("ClientMediaId", clientMediaId);
+		paramMap.put("TotalLen", fileSize);
+		paramMap.put("StartPos", 0);
+		paramMap.put("DataLen", fileSize);
+		paramMap.put("MediaType", 4);
+
+		MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+		builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
+
+		builder.addTextBody("id", "WU_FILE_0", ContentType.TEXT_PLAIN);
+		builder.addTextBody("name", filePath, ContentType.TEXT_PLAIN);
+		builder.addTextBody("type", mimeType, ContentType.TEXT_PLAIN);
+		builder.addTextBody("lastModifieDate", lastModifieDate, ContentType.TEXT_PLAIN);
+		builder.addTextBody("size", String.valueOf(fileSize), ContentType.TEXT_PLAIN);
+		builder.addTextBody("mediatype", mediaType, ContentType.TEXT_PLAIN);
+		builder.addTextBody("uploadmediarequest", JSON.toJSONString(paramMap), ContentType.TEXT_PLAIN);
+		builder.addTextBody("webwx_data_ticket", webwxDataTicket, ContentType.TEXT_PLAIN);
+		builder.addTextBody("pass_ticket", passTicket, ContentType.TEXT_PLAIN);
+		builder.addBinaryBody("filename", file, ContentType.create(mimeType), filePath);
+		HttpEntity reqEntity = builder.build();
+		HttpEntity entity = myHttpClient.doPostFile(url, reqEntity);
+		if (entity != null) {
+			try {
+				String result = EntityUtils.toString(entity, Consts.UTF_8);
+				return JSON.parseObject(result);
+			} catch (Exception e) {
+				LOG.error("webWxUploadMedia 错误: ", e);
+			}
+
+		}
+		return null;
+	}
+
+	/**
+	 * 根据NickName发送图片消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月7日 下午10:32:45
+	 * @param nackName
+	 * @return
+	 */
+	public static boolean sendPicMsgByNickName(String nickName, String filePath) {
+		String toUserName = WechatTools.getUserNameByNickName(nickName);
+		if (toUserName != null) {
+			return sendPicMsgByUserId(toUserName, filePath);
+		}
+		return false;
+	}
+
+	/**
+	 * 根据用户id发送图片消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月7日 下午10:34:24
+	 * @param userId
+	 * @param filePath
+	 * @return
+	 */
+	public static boolean sendPicMsgByUserId(String userId, String filePath) {
+		JSONObject responseObj = webWxUploadMedia(filePath);
+		if (responseObj != null) {
+			String mediaId = responseObj.getString("MediaId");
+			if (mediaId != null) {
+				return webWxSendMsgImg(userId, mediaId);
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * 发送图片消息,内部调用
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月7日 下午10:38:55
+	 * @return
+	 */
+	private static boolean webWxSendMsgImg(String userId, String mediaId) {
+		String url = String.format("%s/webwxsendmsgimg?fun=async&f=json&pass_ticket=%s", core.getLoginInfo().get("url"),
+				core.getLoginInfo().get("pass_ticket"));
+		Map<String, Object> msgMap = new HashMap<String, Object>();
+		msgMap.put("Type", 3);
+		msgMap.put("MediaId", mediaId);
+		msgMap.put("FromUserName", core.getUserSelf().getString("UserName"));
+		msgMap.put("ToUserName", userId);
+		String clientMsgId = String.valueOf(new Date().getTime())
+				+ String.valueOf(new Random().nextLong()).substring(1, 5);
+		msgMap.put("LocalID", clientMsgId);
+		msgMap.put("ClientMsgId", clientMsgId);
+		Map<String, Object> paramMap = core.getParamMap();
+		paramMap.put("BaseRequest", core.getParamMap().get("BaseRequest"));
+		paramMap.put("Msg", msgMap);
+		String paramStr = JSON.toJSONString(paramMap);
+		HttpEntity entity = myHttpClient.doPost(url, paramStr);
+		if (entity != null) {
+			try {
+				String result = EntityUtils.toString(entity, Consts.UTF_8);
+				return JSON.parseObject(result).getJSONObject("BaseResponse").getInteger("Ret") == 0;
+			} catch (Exception e) {
+				LOG.error("webWxSendMsgImg 错误: ", e);
+			}
+		}
+		return false;
+
+	}
+
+	/**
+	 * 根据用户id发送文件
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月7日 下午11:57:36
+	 * @param userId
+	 * @param filePath
+	 * @return
+	 */
+	public static boolean sendFileMsgByUserId(String userId, String filePath) {
+		String title = new File(filePath).getName();
+		Map<String, String> data = new HashMap<String, String>();
+		data.put("appid", Config.API_WXAPPID);
+		data.put("title", title);
+		data.put("totallen", "");
+		data.put("attachid", "");
+		data.put("type", "6"); // APPMSGTYPE_ATTACH
+		data.put("fileext", title.split("\\.")[1]); // 文件后缀
+		JSONObject responseObj = webWxUploadMedia(filePath);
+		if (responseObj != null) {
+			data.put("totallen", responseObj.getString("StartPos"));
+			data.put("attachid", responseObj.getString("MediaId"));
+		} else {
+			LOG.error("sednFileMsgByUserId 错误: ", data);
+		}
+		return webWxSendAppMsg(userId, data);
+	}
+
+	/**
+	 * 根据用户昵称发送文件消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月10日 下午10:59:27
+	 * @param nickName
+	 * @param filePath
+	 * @return
+	 */
+	public static boolean sendFileMsgByNickName(String nickName, String filePath) {
+		String toUserName = WechatTools.getUserNameByNickName(nickName);
+		if (toUserName != null) {
+			return sendFileMsgByUserId(toUserName, filePath);
+		}
+		return false;
+	}
+
+	/**
+	 * 内部调用
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月10日 上午12:21:28
+	 * @param userId
+	 * @param data
+	 * @return
+	 */
+	private static boolean webWxSendAppMsg(String userId, Map<String, String> data) {
+		String url = String.format("%s/webwxsendappmsg?fun=async&f=json&pass_ticket=%s", core.getLoginInfo().get("url"),
+				core.getLoginInfo().get("pass_ticket"));
+		String clientMsgId = String.valueOf(new Date().getTime())
+				+ String.valueOf(new Random().nextLong()).substring(1, 5);
+		String content = "<appmsg appid='wxeb7ec651dd0aefa9' sdkver=''><title>" + data.get("title")
+				+ "</title><des></des><action></action><type>6</type><content></content><url></url><lowurl></lowurl>"
+				+ "<appattach><totallen>" + data.get("totallen") + "</totallen><attachid>" + data.get("attachid")
+				+ "</attachid><fileext>" + data.get("fileext") + "</fileext></appattach><extinfo></extinfo></appmsg>";
+		Map<String, Object> msgMap = new HashMap<String, Object>();
+		msgMap.put("Type", data.get("type"));
+		msgMap.put("Content", content);
+		msgMap.put("FromUserName", core.getUserSelf().getString("UserName"));
+		msgMap.put("ToUserName", userId);
+		msgMap.put("LocalID", clientMsgId);
+		msgMap.put("ClientMsgId", clientMsgId);
+		/*
+		 * Map<String, Object> paramMap = new HashMap<String, Object>();
+		 *
+		 * @SuppressWarnings("unchecked") Map<String, Map<String, String>>
+		 * baseRequestMap = (Map<String, Map<String, String>>)
+		 * core.getLoginInfo() .get("baseRequest"); paramMap.put("BaseRequest",
+		 * baseRequestMap.get("BaseRequest"));
+		 */
+
+		Map<String, Object> paramMap = core.getParamMap();
+		paramMap.put("Msg", msgMap);
+		paramMap.put("Scene", 0);
+		String paramStr = JSON.toJSONString(paramMap);
+		HttpEntity entity = myHttpClient.doPost(url, paramStr);
+		if (entity != null) {
+			try {
+				String result = EntityUtils.toString(entity, Consts.UTF_8);
+				return JSON.parseObject(result).getJSONObject("BaseResponse").getInteger("Ret") == 0;
+			} catch (Exception e) {
+				LOG.error("错误: ", e);
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * 被动添加好友
+	 *
+	 * @date 2017年6月29日 下午10:08:43
+	 * @param msg
+	 * @param accept
+	 *            true 接受 false 拒绝
+	 */
+	public static void addFriend(BaseMsg msg, boolean accept) {
+		if (!accept) { // 不添加
+			return;
+		}
+		int status = VerifyFriendEnum.ACCEPT.getCode(); // 接受好友请求
+		RecommendInfo recommendInfo = msg.getRecommendInfo();
+		String userName = recommendInfo.getUserName();
+		String ticket = recommendInfo.getTicket();
+		// 更新好友列表
+		// TODO 此处需要更新好友列表
+		// core.getContactList().add(msg.getJSONObject("RecommendInfo"));
+
+		String url = String.format(URLEnum.WEB_WX_VERIFYUSER.getUrl(), core.getLoginInfo().get("url"),
+				String.valueOf(System.currentTimeMillis() / 3158L), core.getLoginInfo().get("pass_ticket"));
+
+		List<Map<String, Object>> verifyUserList = new ArrayList<Map<String, Object>>();
+		Map<String, Object> verifyUser = new HashMap<String, Object>();
+		verifyUser.put("Value", userName);
+		verifyUser.put("VerifyUserTicket", ticket);
+		verifyUserList.add(verifyUser);
+
+		List<Integer> sceneList = new ArrayList<Integer>();
+		sceneList.add(33);
+
+		JSONObject body = new JSONObject();
+		body.put("BaseRequest", core.getParamMap().get("BaseRequest"));
+		body.put("Opcode", status);
+		body.put("VerifyUserListSize", 1);
+		body.put("VerifyUserList", verifyUserList);
+		body.put("VerifyContent", "");
+		body.put("SceneListCount", 1);
+		body.put("SceneList", sceneList);
+		body.put("skey", core.getLoginInfo().get(StorageLoginInfoEnum.skey.getKey()));
+
+		String result = null;
+		try {
+			String paramStr = JSON.toJSONString(body);
+			HttpEntity entity = myHttpClient.doPost(url, paramStr);
+			result = EntityUtils.toString(entity, Consts.UTF_8);
+		} catch (Exception e) {
+			LOG.error("webWxSendMsg", e);
+		}
+
+		if (StringUtils.isBlank(result)) {
+			LOG.error("被动添加好友失败");
+		}
+
+		LOG.debug(result);
+
+	}
+
+}

+ 215 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/api/WechatTools.java

@@ -0,0 +1,215 @@
+package com.xmzs.common.wechat.api;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.http.Consts;
+import org.apache.http.HttpEntity;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+import com.xmzs.common.wechat.core.Core;
+import com.xmzs.common.wechat.utils.enums.StorageLoginInfoEnum;
+import com.xmzs.common.wechat.utils.enums.URLEnum;
+
+/**
+ * 微信小工具,如获好友列表等
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年5月4日 下午10:49:16
+ * @version 1.0
+ *
+ */
+public class WechatTools {
+	private static Logger LOG = LoggerFactory.getLogger(WechatTools.class);
+
+	private static Core core = Core.getInstance();
+
+	/**
+	 * 根据用户名发送文本消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月4日 下午10:43:14
+	 * @param msg
+	 * @param toUserName
+	 */
+	public static void sendMsgByUserName(String msg, String toUserName) {
+		MessageTools.sendMsgById(msg, toUserName);
+	}
+
+	/**
+	 * <p>
+	 * 通过RealName获取本次UserName
+	 * </p>
+	 * <p>
+	 * 如NickName为"yaphone",则获取UserName=
+	 * "@1212d3356aea8285e5bbe7b91229936bc183780a8ffa469f2d638bf0d2e4fc63",
+	 * 可通过UserName发送消息
+	 * </p>
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月4日 下午10:56:31
+	 * @param name
+	 * @return
+	 */
+	public static String getUserNameByNickName(String nickName) {
+		for (JSONObject o : core.getContactList()) {
+			if (o.getString("NickName").equals(nickName)) {
+				return o.getString("UserName");
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * 返回好友昵称列表
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月4日 下午11:37:20
+	 * @return
+	 */
+	public static List<String> getContactNickNameList() {
+		List<String> contactNickNameList = new ArrayList<String>();
+		for (JSONObject o : core.getContactList()) {
+			contactNickNameList.add(o.getString("NickName"));
+		}
+		return contactNickNameList;
+	}
+
+	/**
+	 * 返回好友完整信息列表
+	 *
+	 * @date 2017年6月26日 下午9:45:39
+	 * @return
+	 */
+	public static List<JSONObject> getContactList() {
+		return core.getContactList();
+	}
+
+	/**
+	 * 返回群列表
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月5日 下午9:55:21
+	 * @return
+	 */
+	public static List<JSONObject> getGroupList() {
+		return core.getGroupList();
+	}
+
+	/**
+	 * 获取群ID列表
+	 *
+	 * @date 2017年6月21日 下午11:42:56
+	 * @return
+	 */
+	public static List<String> getGroupIdList() {
+		return core.getGroupIdList();
+	}
+
+	/**
+	 * 获取群NickName列表
+	 *
+	 * @date 2017年6月21日 下午11:43:38
+	 * @return
+	 */
+	public static List<String> getGroupNickNameList() {
+		return core.getGroupNickNameList();
+	}
+
+	/**
+	 * 根据groupIdList返回群成员列表
+	 *
+	 * @date 2017年6月13日 下午11:12:31
+	 * @param groupId
+	 * @return
+	 */
+	public static JSONArray getMemberListByGroupId(String groupId) {
+		return core.getGroupMemeberMap().get(groupId);
+	}
+
+	/**
+	 * 退出微信
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月18日 下午11:56:54
+	 */
+	public static void logout() {
+		webWxLogout();
+	}
+
+	private static boolean webWxLogout() {
+		String url = String.format(URLEnum.WEB_WX_LOGOUT.getUrl(),
+				core.getLoginInfo().get(StorageLoginInfoEnum.url.getKey()));
+		List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
+		params.add(new BasicNameValuePair("redirect", "1"));
+		params.add(new BasicNameValuePair("type", "1"));
+		params.add(
+				new BasicNameValuePair("skey", (String) core.getLoginInfo().get(StorageLoginInfoEnum.skey.getKey())));
+		try {
+			HttpEntity entity = core.getMyHttpClient().doGet(url, params, false, null);
+			String text = EntityUtils.toString(entity, Consts.UTF_8); // 无消息
+			return true;
+		} catch (Exception e) {
+			LOG.debug(e.getMessage());
+		}
+		return false;
+	}
+
+	public static void setUserInfo() {
+		for (JSONObject o : core.getContactList()) {
+			core.getUserInfoMap().put(o.getString("NickName"), o);
+			core.getUserInfoMap().put(o.getString("UserName"), o);
+		}
+	}
+
+	/**
+	 *
+	 * 根据用户昵称设置备注名称
+	 *
+	 * @date 2017年5月27日 上午12:21:40
+	 * @param userName
+	 * @param remName
+	 */
+	public static void remarkNameByNickName(String nickName, String remName) {
+		String url = String.format(URLEnum.WEB_WX_REMARKNAME.getUrl(), core.getLoginInfo().get("url"),
+				core.getLoginInfo().get(StorageLoginInfoEnum.pass_ticket.getKey()));
+		Map<String, Object> msgMap = new HashMap<String, Object>();
+		Map<String, Object> msgMap_BaseRequest = new HashMap<String, Object>();
+		msgMap.put("CmdId", 2);
+		msgMap.put("RemarkName", remName);
+		msgMap.put("UserName", core.getUserInfoMap().get(nickName).get("UserName"));
+		msgMap_BaseRequest.put("Uin", core.getLoginInfo().get(StorageLoginInfoEnum.wxuin.getKey()));
+		msgMap_BaseRequest.put("Sid", core.getLoginInfo().get(StorageLoginInfoEnum.wxsid.getKey()));
+		msgMap_BaseRequest.put("Skey", core.getLoginInfo().get(StorageLoginInfoEnum.skey.getKey()));
+		msgMap_BaseRequest.put("DeviceID", core.getLoginInfo().get(StorageLoginInfoEnum.deviceid.getKey()));
+		msgMap.put("BaseRequest", msgMap_BaseRequest);
+		try {
+			String paramStr = JSON.toJSONString(msgMap);
+			HttpEntity entity = core.getMyHttpClient().doPost(url, paramStr);
+			// String result = EntityUtils.toString(entity, Consts.UTF_8);
+			LOG.info("修改备注" + remName);
+		} catch (Exception e) {
+			LOG.error("remarkNameByUserName", e);
+		}
+	}
+
+	/**
+	 * 获取微信在线状态
+	 *
+	 * @date 2017年6月16日 上午12:47:46
+	 * @return
+	 */
+	public static boolean getWechatStatus() {
+		return core.isAlive();
+	}
+
+}

+ 37 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/beans/AppInfo.java

@@ -0,0 +1,37 @@
+package com.xmzs.common.wechat.beans;
+
+import java.io.Serializable;
+
+/**
+ * AppInfo
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年7月3日 下午10:38:14
+ * @version 1.0
+ *
+ */
+public class AppInfo implements Serializable {
+	/**
+	 *
+	 */
+	private static final long serialVersionUID = 1L;
+	private int type;
+	private String appId;
+
+	public int getType() {
+		return type;
+	}
+
+	public void setType(int type) {
+		this.type = type;
+	}
+
+	public String getAppId() {
+		return appId;
+	}
+
+	public void setAppId(String appId) {
+		this.appId = appId;
+	}
+
+}

+ 293 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/beans/BaseMsg.java

@@ -0,0 +1,293 @@
+package com.xmzs.common.wechat.beans;
+
+import java.io.Serializable;
+
+/**
+ * 收到的微信消息
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年7月3日 下午10:28:06
+ * @version 1.0
+ *
+ */
+public class BaseMsg implements Serializable {
+	/**
+	 *
+	 */
+	private static final long serialVersionUID = 1L;
+	private int subMsgType;
+	private int voiceLength;
+	private String fileName;
+	private int imgHeight;
+	private String toUserName;
+	private int hasProductId;
+	private int imgStatus;
+	private String url;
+	private int imgWidth;
+	private int forwardFlag;
+	private int status;
+	private String Ticket;
+	/** 推荐消息报文 **/
+	private RecommendInfo recommendInfo;
+	private long createTime;
+	private String newMsgId;
+	/** 文本消息内容 **/
+	private String text;
+	/** 消息类型 **/
+	private int msgType;
+	/** 是否为群消息 **/
+	private boolean groupMsg;
+	private String msgId;
+	private int statusNotifyCode;
+	private AppInfo appInfo;
+	private int appMsgType;
+	private String Type;
+	private int playLength;
+	private String mediaId;
+	private String content;
+	private String statusNotifyUserName;
+	/** 消息发送者ID **/
+	private String fromUserName;
+	private String oriContent;
+	private String fileSize;
+
+	public int getSubMsgType() {
+		return subMsgType;
+	}
+
+	public void setSubMsgType(int subMsgType) {
+		this.subMsgType = subMsgType;
+	}
+
+	public int getVoiceLength() {
+		return voiceLength;
+	}
+
+	public void setVoiceLength(int voiceLength) {
+		this.voiceLength = voiceLength;
+	}
+
+	public String getFileName() {
+		return fileName;
+	}
+
+	public void setFileName(String fileName) {
+		this.fileName = fileName;
+	}
+
+	public int getImgHeight() {
+		return imgHeight;
+	}
+
+	public void setImgHeight(int imgHeight) {
+		this.imgHeight = imgHeight;
+	}
+
+	public String getToUserName() {
+		return toUserName;
+	}
+
+	public void setToUserName(String toUserName) {
+		this.toUserName = toUserName;
+	}
+
+	public int getHasProductId() {
+		return hasProductId;
+	}
+
+	public void setHasProductId(int hasProductId) {
+		this.hasProductId = hasProductId;
+	}
+
+	public int getImgStatus() {
+		return imgStatus;
+	}
+
+	public void setImgStatus(int imgStatus) {
+		this.imgStatus = imgStatus;
+	}
+
+	public String getUrl() {
+		return url;
+	}
+
+	public void setUrl(String url) {
+		this.url = url;
+	}
+
+	public int getImgWidth() {
+		return imgWidth;
+	}
+
+	public void setImgWidth(int imgWidth) {
+		this.imgWidth = imgWidth;
+	}
+
+	public int getForwardFlag() {
+		return forwardFlag;
+	}
+
+	public void setForwardFlag(int forwardFlag) {
+		this.forwardFlag = forwardFlag;
+	}
+
+	public int getStatus() {
+		return status;
+	}
+
+	public void setStatus(int status) {
+		this.status = status;
+	}
+
+	public String getTicket() {
+		return Ticket;
+	}
+
+	public void setTicket(String ticket) {
+		Ticket = ticket;
+	}
+
+	public RecommendInfo getRecommendInfo() {
+		return recommendInfo;
+	}
+
+	public void setRecommendInfo(RecommendInfo recommendInfo) {
+		this.recommendInfo = recommendInfo;
+	}
+
+	public long getCreateTime() {
+		return createTime;
+	}
+
+	public void setCreateTime(long createTime) {
+		this.createTime = createTime;
+	}
+
+	public String getNewMsgId() {
+		return newMsgId;
+	}
+
+	public void setNewMsgId(String newMsgId) {
+		this.newMsgId = newMsgId;
+	}
+
+	public String getText() {
+		return text;
+	}
+
+	public void setText(String text) {
+		this.text = text;
+	}
+
+	public int getMsgType() {
+		return msgType;
+	}
+
+	public void setMsgType(int msgType) {
+		this.msgType = msgType;
+	}
+
+	public boolean isGroupMsg() {
+		return groupMsg;
+	}
+
+	public void setGroupMsg(boolean groupMsg) {
+		this.groupMsg = groupMsg;
+	}
+
+	public String getMsgId() {
+		return msgId;
+	}
+
+	public void setMsgId(String msgId) {
+		this.msgId = msgId;
+	}
+
+	public int getStatusNotifyCode() {
+		return statusNotifyCode;
+	}
+
+	public void setStatusNotifyCode(int statusNotifyCode) {
+		this.statusNotifyCode = statusNotifyCode;
+	}
+
+	public AppInfo getAppInfo() {
+		return appInfo;
+	}
+
+	public void setAppInfo(AppInfo appInfo) {
+		this.appInfo = appInfo;
+	}
+
+	public int getAppMsgType() {
+		return appMsgType;
+	}
+
+	public void setAppMsgType(int appMsgType) {
+		this.appMsgType = appMsgType;
+	}
+
+	public String getType() {
+		return Type;
+	}
+
+	public void setType(String type) {
+		Type = type;
+	}
+
+	public int getPlayLength() {
+		return playLength;
+	}
+
+	public void setPlayLength(int playLength) {
+		this.playLength = playLength;
+	}
+
+	public String getMediaId() {
+		return mediaId;
+	}
+
+	public void setMediaId(String mediaId) {
+		this.mediaId = mediaId;
+	}
+
+	public String getContent() {
+		return content;
+	}
+
+	public void setContent(String content) {
+		this.content = content;
+	}
+
+	public String getStatusNotifyUserName() {
+		return statusNotifyUserName;
+	}
+
+	public void setStatusNotifyUserName(String statusNotifyUserName) {
+		this.statusNotifyUserName = statusNotifyUserName;
+	}
+
+	public String getFromUserName() {
+		return fromUserName;
+	}
+
+	public void setFromUserName(String fromUserName) {
+		this.fromUserName = fromUserName;
+	}
+
+	public String getOriContent() {
+		return oriContent;
+	}
+
+	public void setOriContent(String oriContent) {
+		this.oriContent = oriContent;
+	}
+
+	public String getFileSize() {
+		return fileSize;
+	}
+
+	public void setFileSize(String fileSize) {
+		this.fileSize = fileSize;
+	}
+}

+ 146 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/beans/RecommendInfo.java

@@ -0,0 +1,146 @@
+package com.xmzs.common.wechat.beans;
+
+import java.io.Serializable;
+
+/**
+ * RecommendInfo
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年7月3日 下午10:35:14
+ * @version 1.0
+ *
+ */
+public class RecommendInfo implements Serializable {
+	/**
+	 *
+	 */
+	private static final long serialVersionUID = 1L;
+
+	private String ticket;
+	private String userName;
+	private int sex;
+	private int attrStatus;
+	private String city;
+	private String nickName;
+	private int scene;
+	private String province;
+	private String content;
+	private String alias;
+	private String signature;
+	private int opCode;
+	private int qQNum;
+	private int verifyFlag;
+
+	public String getTicket() {
+		return ticket;
+	}
+
+	public void setTicket(String ticket) {
+		this.ticket = ticket;
+	}
+
+	public String getUserName() {
+		return userName;
+	}
+
+	public void setUserName(String userName) {
+		this.userName = userName;
+	}
+
+	public int getSex() {
+		return sex;
+	}
+
+	public void setSex(int sex) {
+		this.sex = sex;
+	}
+
+	public int getAttrStatus() {
+		return attrStatus;
+	}
+
+	public void setAttrStatus(int attrStatus) {
+		this.attrStatus = attrStatus;
+	}
+
+	public String getCity() {
+		return city;
+	}
+
+	public void setCity(String city) {
+		this.city = city;
+	}
+
+	public String getNickName() {
+		return nickName;
+	}
+
+	public void setNickName(String nickName) {
+		this.nickName = nickName;
+	}
+
+	public int getScene() {
+		return scene;
+	}
+
+	public void setScene(int scene) {
+		this.scene = scene;
+	}
+
+	public String getProvince() {
+		return province;
+	}
+
+	public void setProvince(String province) {
+		this.province = province;
+	}
+
+	public String getContent() {
+		return content;
+	}
+
+	public void setContent(String content) {
+		this.content = content;
+	}
+
+	public String getAlias() {
+		return alias;
+	}
+
+	public void setAlias(String alias) {
+		this.alias = alias;
+	}
+
+	public String getSignature() {
+		return signature;
+	}
+
+	public void setSignature(String signature) {
+		this.signature = signature;
+	}
+
+	public int getOpCode() {
+		return opCode;
+	}
+
+	public void setOpCode(int opCode) {
+		this.opCode = opCode;
+	}
+
+	public int getqQNum() {
+		return qQNum;
+	}
+
+	public void setqQNum(int qQNum) {
+		this.qQNum = qQNum;
+	}
+
+	public int getVerifyFlag() {
+		return verifyFlag;
+	}
+
+	public void setVerifyFlag(int verifyFlag) {
+		this.verifyFlag = verifyFlag;
+	}
+
+}

+ 90 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/controller/LoginController.java

@@ -0,0 +1,90 @@
+package com.xmzs.common.wechat.controller;
+
+
+import com.xmzs.common.wechat.utils.SleepUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.xmzs.common.wechat.api.WechatTools;
+import com.xmzs.common.wechat.core.Core;
+import com.xmzs.common.wechat.service.ILoginService;
+import com.xmzs.common.wechat.service.impl.LoginServiceImpl;
+import com.xmzs.common.wechat.thread.CheckLoginStatusThread;
+
+import com.xmzs.common.wechat.utils.tools.CommonTools;
+
+/**
+ * 登陆控制器
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年5月13日 下午12:56:07
+ * @version 1.0
+ *
+ */
+public class LoginController {
+	private static Logger LOG = LoggerFactory.getLogger(LoginController.class);
+	private ILoginService loginService = new LoginServiceImpl();
+	private static Core core = Core.getInstance();
+
+	public void login(String qrPath) {
+		if (core.isAlive()) { // 已登陆
+			LOG.info("itchat4j已登陆");
+			return;
+		}
+		while (true) {
+			for (int count = 0; count < 10; count++) {
+				LOG.info("获取UUID");
+				while (loginService.getUuid() == null) {
+					LOG.info("1. 获取微信UUID");
+					while (loginService.getUuid() == null) {
+						LOG.warn("1.1. 获取微信UUID失败,两秒后重新获取");
+						SleepUtils.sleep(2000);
+					}
+				}
+				LOG.info("2. 获取登陆二维码图片");
+				if (loginService.getQR(qrPath)) {
+					break;
+				} else if (count == 10) {
+					LOG.error("2.2. 获取登陆二维码图片失败,系统退出");
+					System.exit(0);
+				}
+			}
+			LOG.info("3. 请扫描二维码图片,并在手机上确认");
+			if (!core.isAlive()) {
+				loginService.login();
+				core.setAlive(true);
+				LOG.info(("登陆成功"));
+				break;
+			}
+			LOG.info("4. 登陆超时,请重新扫描二维码图片");
+		}
+
+		LOG.info("5. 登陆成功,微信初始化");
+		if (!loginService.webWxInit()) {
+			LOG.info("6. 微信初始化异常");
+			System.exit(0);
+		}
+
+		LOG.info("6. 开启微信状态通知");
+		loginService.wxStatusNotify();
+
+		LOG.info("7. 清除。。。。");
+		CommonTools.clearScreen();
+		LOG.info(String.format("欢迎回来, %s", core.getNickName()));
+
+		LOG.info("8. 开始接收消息");
+		loginService.startReceiving();
+
+		LOG.info("9. 获取联系人信息");
+		loginService.webWxGetContact();
+
+		LOG.info("10. 获取群好友及群好友列表");
+		loginService.WebWxBatchGetContact();
+
+		LOG.info("11. 缓存本次登陆好友相关消息");
+		WechatTools.setUserInfo(); // 登陆成功后缓存本次登陆好友相关消息(NickName, UserName)
+
+		LOG.info("12.开启微信状态检测线程");
+		new Thread(new CheckLoginStatusThread()).start();
+	}
+}

+ 276 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/core/Core.java

@@ -0,0 +1,276 @@
+package com.xmzs.common.wechat.core;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+import com.xmzs.common.wechat.beans.BaseMsg;
+import com.xmzs.common.wechat.utils.MyHttpClient;
+import com.xmzs.common.wechat.utils.enums.parameters.BaseParaEnum;
+
+/**
+ * 核心存储类,全局只保存一份,单例模式
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年4月23日 下午2:33:56
+ * @version 1.0
+ *
+ */
+public class Core {
+
+	private static Core instance;
+
+	private Core() {
+
+	}
+
+	public static Core getInstance() {
+		if (instance == null) {
+			synchronized (Core.class) {
+				instance = new Core();
+			}
+		}
+		return instance;
+	}
+
+	boolean alive = false;
+	private int memberCount = 0;
+
+	private String indexUrl;
+
+	private String userName;
+	private String nickName;
+	private List<BaseMsg> msgList = new ArrayList<BaseMsg>();
+
+	private JSONObject userSelf; // 登陆账号自身信息
+	private List<JSONObject> memberList = new ArrayList<JSONObject>(); // 好友+群聊+公众号+特殊账号
+	private List<JSONObject> contactList = new ArrayList<JSONObject>();// 好友
+	private List<JSONObject> groupList = new ArrayList<JSONObject>();; // 群
+	private Map<String, JSONArray> groupMemeberMap = new HashMap<String, JSONArray>(); // 群聊成员字典
+	private List<JSONObject> publicUsersList = new ArrayList<JSONObject>();;// 公众号/服务号
+	private List<JSONObject> specialUsersList = new ArrayList<JSONObject>();;// 特殊账号
+	private List<String> groupIdList = new ArrayList<String>(); // 群ID列表
+	private List<String> groupNickNameList = new ArrayList<String>(); // 群NickName列表
+
+	private Map<String, JSONObject> userInfoMap = new HashMap<String, JSONObject>();
+
+	Map<String, Object> loginInfo = new HashMap<String, Object>();
+	// CloseableHttpClient httpClient = HttpClients.createDefault();
+	MyHttpClient myHttpClient = MyHttpClient.getInstance();
+	String uuid = null;
+
+	boolean useHotReload = false;
+	String hotReloadDir = "itchat.pkl";
+	int receivingRetryCount = 5;
+
+	private long lastNormalRetcodeTime; // 最后一次收到正常retcode的时间,秒为单位
+
+	/**
+	 * 请求参数
+	 */
+	public Map<String, Object> getParamMap() {
+		return new HashMap<String, Object>(1) {
+			/**
+			 *
+			 */
+			private static final long serialVersionUID = 1L;
+
+			{
+				Map<String, String> map = new HashMap<String, String>();
+				for (BaseParaEnum baseRequest : BaseParaEnum.values()) {
+					map.put(baseRequest.para(), getLoginInfo().get(baseRequest.value()).toString());
+				}
+				put("BaseRequest", map);
+			}
+		};
+	}
+
+	public boolean isAlive() {
+		return alive;
+	}
+
+	public void setAlive(boolean alive) {
+		this.alive = alive;
+	}
+
+	public List<JSONObject> getMemberList() {
+		return memberList;
+	}
+
+	public void setMemberList(List<JSONObject> memberList) {
+		this.memberList = memberList;
+	}
+
+	public Map<String, Object> getLoginInfo() {
+		return loginInfo;
+	}
+
+	public void setLoginInfo(Map<String, Object> loginInfo) {
+		this.loginInfo = loginInfo;
+	}
+
+	public String getUuid() {
+		return uuid;
+	}
+
+	public void setUuid(String uuid) {
+		this.uuid = uuid;
+	}
+
+	public int getMemberCount() {
+		return memberCount;
+	}
+
+	public void setMemberCount(int memberCount) {
+		this.memberCount = memberCount;
+	}
+
+	public boolean isUseHotReload() {
+		return useHotReload;
+	}
+
+	public void setUseHotReload(boolean useHotReload) {
+		this.useHotReload = useHotReload;
+	}
+
+	public String getHotReloadDir() {
+		return hotReloadDir;
+	}
+
+	public void setHotReloadDir(String hotReloadDir) {
+		this.hotReloadDir = hotReloadDir;
+	}
+
+	public int getReceivingRetryCount() {
+		return receivingRetryCount;
+	}
+
+	public void setReceivingRetryCount(int receivingRetryCount) {
+		this.receivingRetryCount = receivingRetryCount;
+	}
+
+	public MyHttpClient getMyHttpClient() {
+		return myHttpClient;
+	}
+
+	public List<BaseMsg> getMsgList() {
+		return msgList;
+	}
+
+	public void setMsgList(List<BaseMsg> msgList) {
+		this.msgList = msgList;
+	}
+
+	public void setMyHttpClient(MyHttpClient myHttpClient) {
+		this.myHttpClient = myHttpClient;
+	}
+
+	public List<String> getGroupIdList() {
+		return groupIdList;
+	}
+
+	public void setGroupIdList(List<String> groupIdList) {
+		this.groupIdList = groupIdList;
+	}
+
+	public List<JSONObject> getContactList() {
+		return contactList;
+	}
+
+	public void setContactList(List<JSONObject> contactList) {
+		this.contactList = contactList;
+	}
+
+	public List<JSONObject> getGroupList() {
+		return groupList;
+	}
+
+	public void setGroupList(List<JSONObject> groupList) {
+		this.groupList = groupList;
+	}
+
+	public List<JSONObject> getPublicUsersList() {
+		return publicUsersList;
+	}
+
+	public void setPublicUsersList(List<JSONObject> publicUsersList) {
+		this.publicUsersList = publicUsersList;
+	}
+
+	public List<JSONObject> getSpecialUsersList() {
+		return specialUsersList;
+	}
+
+	public void setSpecialUsersList(List<JSONObject> specialUsersList) {
+		this.specialUsersList = specialUsersList;
+	}
+
+	public String getUserName() {
+		return userName;
+	}
+
+	public void setUserName(String userName) {
+		this.userName = userName;
+	}
+
+	public String getNickName() {
+		return nickName;
+	}
+
+	public void setNickName(String nickName) {
+		this.nickName = nickName;
+	}
+
+	public JSONObject getUserSelf() {
+		return userSelf;
+	}
+
+	public void setUserSelf(JSONObject userSelf) {
+		this.userSelf = userSelf;
+	}
+
+	public Map<String, JSONObject> getUserInfoMap() {
+		return userInfoMap;
+	}
+
+	public void setUserInfoMap(Map<String, JSONObject> userInfoMap) {
+		this.userInfoMap = userInfoMap;
+	}
+
+	public synchronized long getLastNormalRetcodeTime() {
+		return lastNormalRetcodeTime;
+	}
+
+	public synchronized void setLastNormalRetcodeTime(long lastNormalRetcodeTime) {
+		this.lastNormalRetcodeTime = lastNormalRetcodeTime;
+	}
+
+	public List<String> getGroupNickNameList() {
+		return groupNickNameList;
+	}
+
+	public void setGroupNickNameList(List<String> groupNickNameList) {
+		this.groupNickNameList = groupNickNameList;
+	}
+
+	public Map<String, JSONArray> getGroupMemeberMap() {
+		return groupMemeberMap;
+	}
+
+	public void setGroupMemeberMap(Map<String, JSONArray> groupMemeberMap) {
+		this.groupMemeberMap = groupMemeberMap;
+	}
+
+	public String getIndexUrl() {
+		return indexUrl;
+	}
+
+	public void setIndexUrl(String indexUrl) {
+		this.indexUrl = indexUrl;
+	}
+
+}

+ 171 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/core/MsgCenter.java

@@ -0,0 +1,171 @@
+package com.xmzs.common.wechat.core;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+
+import com.xmzs.common.wechat.utils.enums.MsgCodeEnum;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+import com.xmzs.common.wechat.api.MessageTools;
+import com.xmzs.common.wechat.beans.BaseMsg;
+import com.xmzs.common.wechat.face.IMsgHandlerFace;
+
+import com.xmzs.common.wechat.utils.enums.MsgTypeEnum;
+import com.xmzs.common.wechat.utils.tools.CommonTools;
+
+/**
+ * 消息处理中心
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年5月14日 下午12:47:50
+ * @version 1.0
+ *
+ */
+public class MsgCenter {
+	private static Logger LOG = LoggerFactory.getLogger(MsgCenter.class);
+
+	private static Core core = Core.getInstance();
+
+	/**
+	 * 接收消息,放入队列
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月23日 下午2:30:48
+	 * @param msgList
+	 * @return
+	 */
+	public static JSONArray produceMsg(JSONArray msgList) {
+		JSONArray result = new JSONArray();
+		for (int i = 0; i < msgList.size(); i++) {
+			JSONObject msg = new JSONObject();
+			JSONObject m = msgList.getJSONObject(i);
+			m.put("groupMsg", false);// 是否是群消息
+			if (m.getString("FromUserName").contains("@@") || m.getString("ToUserName").contains("@@")) { // 群聊消息
+				if (m.getString("FromUserName").contains("@@")
+						&& !core.getGroupIdList().contains(m.getString("FromUserName"))) {
+					core.getGroupIdList().add((m.getString("FromUserName")));
+				} else if (m.getString("ToUserName").contains("@@")
+						&& !core.getGroupIdList().contains(m.getString("ToUserName"))) {
+					core.getGroupIdList().add((m.getString("ToUserName")));
+				}
+				// 群消息与普通消息不同的是在其消息体(Content)中会包含发送者id及":<br/>"消息,这里需要处理一下,去掉多余信息,只保留消息内容
+				if (m.getString("Content").contains("<br/>")) {
+					String content = m.getString("Content").substring(m.getString("Content").indexOf("<br/>") + 5);
+					m.put("Content", content);
+					m.put("groupMsg", true);
+				}
+			} else {
+				CommonTools.msgFormatter(m, "Content");
+			}
+			if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_TEXT.getCode())) { // words
+																						// 文本消息
+				if (m.getString("Url").length() != 0) {
+					String regEx = "(.+?\\(.+?\\))";
+					Matcher matcher = CommonTools.getMatcher(regEx, m.getString("Content"));
+					String data = "Map";
+					if (matcher.find()) {
+						data = matcher.group(1);
+					}
+					msg.put("Type", "Map");
+					msg.put("Text", data);
+				} else {
+					msg.put("Type", MsgTypeEnum.TEXT.getType());
+					msg.put("Text", m.getString("Content"));
+				}
+				m.put("Type", msg.getString("Type"));
+				m.put("Text", msg.getString("Text"));
+			} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_IMAGE.getCode())
+					|| m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_EMOTICON.getCode())) { // 图片消息
+				m.put("Type", MsgTypeEnum.PIC.getType());
+			} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_VOICE.getCode())) { // 语音消息
+				m.put("Type", MsgTypeEnum.VOICE.getType());
+			} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_VERIFYMSG.getCode())) {// friends
+				// 好友确认消息
+				// MessageTools.addFriend(core, userName, 3, ticket); // 确认添加好友
+				m.put("Type", MsgTypeEnum.VERIFYMSG.getType());
+
+			} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_SHARECARD.getCode())) { // 共享名片
+				m.put("Type", MsgTypeEnum.NAMECARD.getType());
+
+			} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_VIDEO.getCode())
+					|| m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_MICROVIDEO.getCode())) {// viedo
+				m.put("Type", MsgTypeEnum.VIEDO.getType());
+			} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_MEDIA.getCode())) { // 多媒体消息
+				m.put("Type", MsgTypeEnum.MEDIA.getType());
+			} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_STATUSNOTIFY.getCode())) {// phone
+				// init
+				// 微信初始化消息
+
+			} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_SYS.getCode())) {// 系统消息
+				m.put("Type", MsgTypeEnum.SYS.getType());
+			} else if (m.getInteger("MsgType").equals(MsgCodeEnum.MSGTYPE_RECALLED.getCode())) { // 撤回消息
+
+			} else {
+				LOG.info("Useless msg");
+			}
+			LOG.info("收到消息一条,来自: " + m.getString("FromUserName"));
+			result.add(m);
+		}
+		return result;
+	}
+
+	/**
+	 * 消息处理
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月14日 上午10:52:34
+	 * @param msgHandler
+	 */
+	public static void handleMsg(IMsgHandlerFace msgHandler) {
+		while (true) {
+			if (core.getMsgList().size() > 0 && core.getMsgList().get(0).getContent() != null) {
+				if (core.getMsgList().get(0).getContent().length() > 0) {
+					BaseMsg msg = core.getMsgList().get(0);
+					if (msg.getType() != null) {
+						try {
+							if (msg.getType().equals(MsgTypeEnum.TEXT.getType())) {
+								String result = msgHandler.textMsgHandle(msg);
+								MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
+							} else if (msg.getType().equals(MsgTypeEnum.PIC.getType())) {
+
+								String result = msgHandler.picMsgHandle(msg);
+								MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
+							} else if (msg.getType().equals(MsgTypeEnum.VOICE.getType())) {
+								String result = msgHandler.voiceMsgHandle(msg);
+								MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
+							} else if (msg.getType().equals(MsgTypeEnum.VIEDO.getType())) {
+								String result = msgHandler.viedoMsgHandle(msg);
+								MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
+							} else if (msg.getType().equals(MsgTypeEnum.NAMECARD.getType())) {
+								String result = msgHandler.nameCardMsgHandle(msg);
+								MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
+							} else if (msg.getType().equals(MsgTypeEnum.SYS.getType())) { // 系统消息
+								msgHandler.sysMsgHandle(msg);
+							} else if (msg.getType().equals(MsgTypeEnum.VERIFYMSG.getType())) { // 确认添加好友消息
+								String result = msgHandler.verifyAddFriendMsgHandle(msg);
+								MessageTools.sendMsgById(result,
+										core.getMsgList().get(0).getRecommendInfo().getUserName());
+							} else if (msg.getType().equals(MsgTypeEnum.MEDIA.getType())) { // 多媒体消息
+								String result = msgHandler.mediaMsgHandle(msg);
+								MessageTools.sendMsgById(result, core.getMsgList().get(0).getFromUserName());
+							}
+						} catch (Exception e) {
+							e.printStackTrace();
+						}
+					}
+				}
+				core.getMsgList().remove(0);
+			}
+			try {
+				TimeUnit.MILLISECONDS.sleep(1000);
+			} catch (InterruptedException e) {
+				e.printStackTrace();
+			}
+		}
+	}
+
+}

+ 91 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/face/IMsgHandlerFace.java

@@ -0,0 +1,91 @@
+package com.xmzs.common.wechat.face;
+
+import com.xmzs.common.wechat.beans.BaseMsg;
+
+/**
+ * 消息处理接口
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年4月20日 上午12:13:49
+ * @version 1.0
+ *
+ */
+public interface IMsgHandlerFace {
+	/**
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月20日 上午12:15:00
+	 * @param msg
+	 * @return
+	 */
+	public String textMsgHandle(BaseMsg msg);
+
+	/**
+	 * 处理图片消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月21日 下午11:07:06
+	 * @param msg
+	 * @return
+	 */
+	public String picMsgHandle(BaseMsg msg);
+
+	/**
+	 * 处理声音消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月22日 上午12:09:44
+	 * @param msg
+	 * @return
+	 */
+	public String voiceMsgHandle(BaseMsg msg);
+
+	/**
+	 * 处理小视频消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月23日 下午12:19:50
+	 * @param msg
+	 * @return
+	 */
+	public String viedoMsgHandle(BaseMsg msg);
+
+	/**
+	 * 处理名片消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月1日 上午12:50:50
+	 * @param msg
+	 * @return
+	 */
+	public String nameCardMsgHandle(BaseMsg msg);
+
+	/**
+	 * 处理系统消息
+	 *
+	 * @author Relyn
+	 * @date 2017年6月21日17:43:51
+	 * @param msg
+	 * @return
+	 */
+	public void sysMsgHandle(BaseMsg msg);
+
+	/**
+	 * 处理确认添加好友消息
+	 *
+	 * @date 2017年6月28日 下午10:15:30
+	 * @param msg
+	 * @return
+	 */
+	public String verifyAddFriendMsgHandle(BaseMsg msg);
+
+	/**
+	 * 处理收到的文件消息
+	 *
+	 * @date 2017年7月21日 下午11:59:14
+	 * @param msg
+	 * @return
+	 */
+	public String mediaMsgHandle(BaseMsg msg);
+
+}

+ 82 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/service/ILoginService.java

@@ -0,0 +1,82 @@
+package com.xmzs.common.wechat.service;
+
+/**
+ * 登陆服务接口
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年5月13日 上午12:07:21
+ * @version 1.0
+ *
+ */
+public interface ILoginService {
+
+	/**
+	 * 登陆
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月13日 上午12:14:07
+	 * @return
+	 */
+	boolean login();
+
+	/**
+	 * 获取UUID
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月13日 上午12:21:40
+	 * @param qrPath
+	 * @return
+	 */
+	String getUuid();
+
+	/**
+	 * 获取二维码图片
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月13日 上午12:13:51
+	 * @param qrPath
+	 * @return
+	 */
+	boolean getQR(String qrPath);
+
+	/**
+	 * web初始化
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月13日 上午12:14:13
+	 * @return
+	 */
+	boolean webWxInit();
+
+	/**
+	 * 微信状态通知
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月13日 上午12:14:24
+	 */
+	void wxStatusNotify();
+
+	/**
+	 * 接收消息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月13日 上午12:14:37
+	 */
+	void startReceiving();
+
+	/**
+	 * 获取微信联系人
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月13日 下午2:26:18
+	 */
+	void webWxGetContact();
+
+	/**
+	 * 批量获取联系人信息
+	 *
+	 * @date 2017年6月22日 下午11:24:35
+	 */
+	void WebWxBatchGetContact();
+
+}

+ 687 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/service/impl/LoginServiceImpl.java

@@ -0,0 +1,687 @@
+package com.xmzs.common.wechat.service.impl;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Random;
+import java.util.regex.Matcher;
+
+import com.xmzs.common.wechat.utils.SleepUtils;
+import org.apache.http.Consts;
+import org.apache.http.HttpEntity;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+import com.xmzs.common.wechat.beans.BaseMsg;
+import com.xmzs.common.wechat.core.Core;
+import com.xmzs.common.wechat.core.MsgCenter;
+import com.xmzs.common.wechat.service.ILoginService;
+import com.xmzs.common.wechat.utils.Config;
+import com.xmzs.common.wechat.utils.MyHttpClient;
+
+import com.xmzs.common.wechat.utils.enums.ResultEnum;
+import com.xmzs.common.wechat.utils.enums.RetCodeEnum;
+import com.xmzs.common.wechat.utils.enums.StorageLoginInfoEnum;
+import com.xmzs.common.wechat.utils.enums.URLEnum;
+import com.xmzs.common.wechat.utils.enums.parameters.BaseParaEnum;
+import com.xmzs.common.wechat.utils.enums.parameters.LoginParaEnum;
+import com.xmzs.common.wechat.utils.enums.parameters.StatusNotifyParaEnum;
+import com.xmzs.common.wechat.utils.enums.parameters.UUIDParaEnum;
+import com.xmzs.common.wechat.utils.tools.CommonTools;
+
+/**
+ * 登陆服务实现类
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年5月13日 上午12:09:35
+ * @version 1.0
+ *
+ */
+public class LoginServiceImpl implements ILoginService {
+	private static Logger LOG = LoggerFactory.getLogger(LoginServiceImpl.class);
+
+	private Core core = Core.getInstance();
+	private MyHttpClient httpClient = core.getMyHttpClient();
+
+	private MyHttpClient myHttpClient = core.getMyHttpClient();
+
+	public LoginServiceImpl() {
+
+	}
+
+	@Override
+	public boolean login() {
+
+		boolean isLogin = false;
+		// 组装参数和URL
+		List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
+		params.add(new BasicNameValuePair(LoginParaEnum.LOGIN_ICON.para(), LoginParaEnum.LOGIN_ICON.value()));
+		params.add(new BasicNameValuePair(LoginParaEnum.UUID.para(), core.getUuid()));
+		params.add(new BasicNameValuePair(LoginParaEnum.TIP.para(), LoginParaEnum.TIP.value()));
+
+		// long time = 4000;
+		while (!isLogin) {
+			// SleepUtils.sleep(time += 1000);
+			long millis = System.currentTimeMillis();
+			params.add(new BasicNameValuePair(LoginParaEnum.R.para(), String.valueOf(millis / 1579L)));
+			params.add(new BasicNameValuePair(LoginParaEnum._1.para(), String.valueOf(millis)));
+			HttpEntity entity = httpClient.doGet(URLEnum.LOGIN_URL.getUrl(), params, true, null);
+
+			try {
+				String result = EntityUtils.toString(entity);
+				String status = checklogin(result);
+
+				if (ResultEnum.SUCCESS.getCode().equals(status)) {
+					processLoginInfo(result); // 处理结果
+					isLogin = true;
+					core.setAlive(isLogin);
+					break;
+				}
+				if (ResultEnum.WAIT_CONFIRM.getCode().equals(status)) {
+					LOG.info("请点击微信确认按钮,进行登陆");
+				}
+
+			} catch (Exception e) {
+				LOG.error("微信登陆异常!", e);
+			}
+		}
+		return isLogin;
+	}
+
+	@Override
+	public String getUuid() {
+		// 组装参数和URL
+		List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
+		params.add(new BasicNameValuePair(UUIDParaEnum.APP_ID.para(), UUIDParaEnum.APP_ID.value()));
+		params.add(new BasicNameValuePair(UUIDParaEnum.REDIRECT_URL.para(),
+				UUIDParaEnum.REDIRECT_URL.value()));
+		params.add(new BasicNameValuePair(UUIDParaEnum.FUN.para(), UUIDParaEnum.FUN.value()));
+		params.add(new BasicNameValuePair(UUIDParaEnum.LANG.para(), UUIDParaEnum.LANG.value()));
+		params.add(new BasicNameValuePair(UUIDParaEnum._1.para(), String.valueOf(System.currentTimeMillis())));
+
+		HttpEntity entity = httpClient.doGet(URLEnum.UUID_URL.getUrl(), params, true, null);
+
+		try {
+			String result = EntityUtils.toString(entity);
+			String regEx = "window.QRLogin.code = (\\d+); window.QRLogin.uuid = \"(\\S+?)\";";
+			Matcher matcher = CommonTools.getMatcher(regEx, result);
+			if (matcher.find()) {
+				if ((ResultEnum.SUCCESS.getCode().equals(matcher.group(1)))) {
+					core.setUuid(matcher.group(2));
+				}
+			}
+		} catch (Exception e) {
+			LOG.error(e.getMessage(), e);
+		}
+
+		return core.getUuid();
+	}
+
+	@Override
+	public boolean getQR(String qrPath) {
+		qrPath = qrPath + File.separator + "QR.jpg";
+		String qrUrl = URLEnum.QRCODE_URL.getUrl() + core.getUuid();
+		HttpEntity entity = myHttpClient.doGet(qrUrl, null, true, null);
+		try {
+			OutputStream out = new FileOutputStream(qrPath);
+			byte[] bytes = EntityUtils.toByteArray(entity);
+			out.write(bytes);
+			out.flush();
+			out.close();
+			try {
+				CommonTools.printQr(qrPath); // 打开登陆二维码图片
+			} catch (Exception e) {
+				LOG.info(e.getMessage());
+			}
+
+		} catch (Exception e) {
+			LOG.info(e.getMessage());
+			return false;
+		}
+
+		return true;
+	}
+
+	@Override
+	public boolean webWxInit() {
+		core.setAlive(true);
+		core.setLastNormalRetcodeTime(System.currentTimeMillis());
+		// 组装请求URL和参数
+		String url = String.format(URLEnum.INIT_URL.getUrl(),
+				core.getLoginInfo().get(StorageLoginInfoEnum.url.getKey()),
+				String.valueOf(System.currentTimeMillis() / 3158L),
+				core.getLoginInfo().get(StorageLoginInfoEnum.pass_ticket.getKey()));
+
+		Map<String, Object> paramMap = core.getParamMap();
+
+		// 请求初始化接口
+		HttpEntity entity = httpClient.doPost(url, JSON.toJSONString(paramMap));
+		try {
+			String result = EntityUtils.toString(entity, Consts.UTF_8);
+			JSONObject obj = JSON.parseObject(result);
+
+			JSONObject user = obj.getJSONObject(StorageLoginInfoEnum.User.getKey());
+			JSONObject syncKey = obj.getJSONObject(StorageLoginInfoEnum.SyncKey.getKey());
+
+			core.getLoginInfo().put(StorageLoginInfoEnum.InviteStartCount.getKey(),
+					obj.getInteger(StorageLoginInfoEnum.InviteStartCount.getKey()));
+			core.getLoginInfo().put(StorageLoginInfoEnum.SyncKey.getKey(), syncKey);
+
+			JSONArray syncArray = syncKey.getJSONArray("List");
+			StringBuilder sb = new StringBuilder();
+			for (int i = 0; i < syncArray.size(); i++) {
+				sb.append(syncArray.getJSONObject(i).getString("Key") + "_"
+						+ syncArray.getJSONObject(i).getString("Val") + "|");
+			}
+			// 1_661706053|2_661706420|3_661706415|1000_1494151022|
+			String synckey = sb.toString();
+
+			// 1_661706053|2_661706420|3_661706415|1000_1494151022
+			core.getLoginInfo().put(StorageLoginInfoEnum.synckey.getKey(), synckey.substring(0, synckey.length() - 1));// 1_656161336|2_656161626|3_656161313|11_656159955|13_656120033|201_1492273724|1000_1492265953|1001_1492250432|1004_1491805192
+			core.setUserName(user.getString("UserName"));
+			core.setNickName(user.getString("NickName"));
+			core.setUserSelf(obj.getJSONObject("User"));
+
+			String chatSet = obj.getString("ChatSet");
+			String[] chatSetArray = chatSet.split(",");
+			for (int i = 0; i < chatSetArray.length; i++) {
+				if (chatSetArray[i].indexOf("@@") != -1) {
+					// 更新GroupIdList
+					core.getGroupIdList().add(chatSetArray[i]); //
+				}
+			}
+			// JSONArray contactListArray = obj.getJSONArray("ContactList");
+			// for (int i = 0; i < contactListArray.size(); i++) {
+			// JSONObject o = contactListArray.getJSONObject(i);
+			// if (o.getString("UserName").indexOf("@@") != -1) {
+			// core.getGroupIdList().add(o.getString("UserName")); //
+			// // 更新GroupIdList
+			// core.getGroupList().add(o); // 更新GroupList
+			// core.getGroupNickNameList().add(o.getString("NickName"));
+			// }
+			// }
+		} catch (Exception e) {
+			e.printStackTrace();
+			return false;
+		}
+		return true;
+	}
+
+	@Override
+	public void wxStatusNotify() {
+		// 组装请求URL和参数
+		String url = String.format(URLEnum.STATUS_NOTIFY_URL.getUrl(),
+				core.getLoginInfo().get(StorageLoginInfoEnum.pass_ticket.getKey()));
+
+		Map<String, Object> paramMap = core.getParamMap();
+		paramMap.put(StatusNotifyParaEnum.CODE.para(), StatusNotifyParaEnum.CODE.value());
+		paramMap.put(StatusNotifyParaEnum.FROM_USERNAME.para(), core.getUserName());
+		paramMap.put(StatusNotifyParaEnum.TO_USERNAME.para(), core.getUserName());
+		paramMap.put(StatusNotifyParaEnum.CLIENT_MSG_ID.para(), System.currentTimeMillis());
+		String paramStr = JSON.toJSONString(paramMap);
+
+		try {
+			HttpEntity entity = httpClient.doPost(url, paramStr);
+			EntityUtils.toString(entity, Consts.UTF_8);
+		} catch (Exception e) {
+			LOG.error("微信状态通知接口失败!", e);
+		}
+
+	}
+
+	@Override
+	public void startReceiving() {
+		core.setAlive(true);
+		new Thread(new Runnable() {
+			int retryCount = 0;
+
+			@Override
+			public void run() {
+				while (core.isAlive()) {
+					try {
+						Map<String, String> resultMap = syncCheck();
+						LOG.info(JSONObject.toJSONString(resultMap));
+						String retcode = resultMap.get("retcode");
+						String selector = resultMap.get("selector");
+						if (retcode.equals(RetCodeEnum.UNKOWN.getCode())) {
+							LOG.info(RetCodeEnum.UNKOWN.getType());
+							continue;
+						} else if (retcode.equals(RetCodeEnum.LOGIN_OUT.getCode())) { // 退出
+							LOG.info(RetCodeEnum.LOGIN_OUT.getType());
+							break;
+						} else if (retcode.equals(RetCodeEnum.LOGIN_OTHERWHERE.getCode())) { // 其它地方登陆
+							LOG.info(RetCodeEnum.LOGIN_OTHERWHERE.getType());
+							break;
+						} else if (retcode.equals(RetCodeEnum.MOBILE_LOGIN_OUT.getCode())) { // 移动端退出
+							LOG.info(RetCodeEnum.MOBILE_LOGIN_OUT.getType());
+							break;
+						} else if (retcode.equals(RetCodeEnum.NORMAL.getCode())) {
+							core.setLastNormalRetcodeTime(System.currentTimeMillis()); // 最后收到正常报文时间
+							JSONObject msgObj = webWxSync();
+							if (selector.equals("2")) {
+								if (msgObj != null) {
+									try {
+										JSONArray msgList = new JSONArray();
+										msgList = msgObj.getJSONArray("AddMsgList");
+										msgList = MsgCenter.produceMsg(msgList);
+										for (int j = 0; j < msgList.size(); j++) {
+											BaseMsg baseMsg = JSON.toJavaObject(msgList.getJSONObject(j),
+													BaseMsg.class);
+											core.getMsgList().add(baseMsg);
+										}
+									} catch (Exception e) {
+										LOG.info(e.getMessage());
+									}
+								}
+							} else if (selector.equals("7")) {
+								webWxSync();
+							} else if (selector.equals("4")) {
+								continue;
+							} else if (selector.equals("3")) {
+								continue;
+							} else if (selector.equals("6")) {
+								if (msgObj != null) {
+									try {
+										JSONArray msgList = new JSONArray();
+										msgList = msgObj.getJSONArray("AddMsgList");
+										JSONArray modContactList = msgObj.getJSONArray("ModContactList"); // 存在删除或者新增的好友信息
+										msgList = MsgCenter.produceMsg(msgList);
+										for (int j = 0; j < msgList.size(); j++) {
+											JSONObject userInfo = modContactList.getJSONObject(j);
+											// 存在主动加好友之后的同步联系人到本地
+											core.getContactList().add(userInfo);
+										}
+									} catch (Exception e) {
+										LOG.info(e.getMessage());
+									}
+								}
+
+							}
+						} else {
+							JSONObject obj = webWxSync();
+						}
+					} catch (Exception e) {
+						LOG.info(e.getMessage());
+						retryCount += 1;
+						if (core.getReceivingRetryCount() < retryCount) {
+							core.setAlive(false);
+						} else {
+							try {
+								Thread.sleep(1000);
+							} catch (InterruptedException e1) {
+								LOG.info(e.getMessage());
+							}
+						}
+					}
+
+				}
+			}
+		}).start();
+
+	}
+
+	@Override
+	public void webWxGetContact() {
+		String url = String.format(URLEnum.WEB_WX_GET_CONTACT.getUrl(),
+				core.getLoginInfo().get(StorageLoginInfoEnum.url.getKey()));
+		Map<String, Object> paramMap = core.getParamMap();
+		HttpEntity entity = httpClient.doPost(url, JSON.toJSONString(paramMap));
+
+		try {
+			String result = EntityUtils.toString(entity, Consts.UTF_8);
+			JSONObject fullFriendsJsonList = JSON.parseObject(result);
+			// 查看seq是否为0,0表示好友列表已全部获取完毕,若大于0,则表示好友列表未获取完毕,当前的字节数(断点续传)
+			long seq = 0;
+			long currentTime = 0L;
+			List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
+			if (fullFriendsJsonList.get("Seq") != null) {
+				seq = fullFriendsJsonList.getLong("Seq");
+				currentTime = new Date().getTime();
+			}
+			core.setMemberCount(fullFriendsJsonList.getInteger(StorageLoginInfoEnum.MemberCount.getKey()));
+			JSONArray member = fullFriendsJsonList.getJSONArray(StorageLoginInfoEnum.MemberList.getKey());
+			// 循环获取seq直到为0,即获取全部好友列表 ==0:好友获取完毕 >0:好友未获取完毕,此时seq为已获取的字节数
+			while (seq > 0) {
+				// 设置seq传参
+				params.add(new BasicNameValuePair("r", String.valueOf(currentTime)));
+				params.add(new BasicNameValuePair("seq", String.valueOf(seq)));
+				entity = httpClient.doGet(url, params, false, null);
+
+				params.remove(new BasicNameValuePair("r", String.valueOf(currentTime)));
+				params.remove(new BasicNameValuePair("seq", String.valueOf(seq)));
+
+				result = EntityUtils.toString(entity, Consts.UTF_8);
+				fullFriendsJsonList = JSON.parseObject(result);
+
+				if (fullFriendsJsonList.get("Seq") != null) {
+					seq = fullFriendsJsonList.getLong("Seq");
+					currentTime = new Date().getTime();
+				}
+
+				// 累加好友列表
+				member.addAll(fullFriendsJsonList.getJSONArray(StorageLoginInfoEnum.MemberList.getKey()));
+			}
+			core.setMemberCount(member.size());
+			for (Iterator<?> iterator = member.iterator(); iterator.hasNext();) {
+				JSONObject o = (JSONObject) iterator.next();
+				if ((o.getInteger("VerifyFlag") & 8) != 0) { // 公众号/服务号
+					core.getPublicUsersList().add(o);
+				} else if (Config.API_SPECIAL_USER.contains(o.getString("UserName"))) { // 特殊账号
+					core.getSpecialUsersList().add(o);
+				} else if (o.getString("UserName").indexOf("@@") != -1) { // 群聊
+					if (!core.getGroupIdList().contains(o.getString("UserName"))) {
+						core.getGroupNickNameList().add(o.getString("NickName"));
+						core.getGroupIdList().add(o.getString("UserName"));
+						core.getGroupList().add(o);
+					}
+				} else if (o.getString("UserName").equals(core.getUserSelf().getString("UserName"))) { // 自己
+					core.getContactList().remove(o);
+				} else { // 普通联系人
+					core.getContactList().add(o);
+				}
+			}
+			return;
+		} catch (Exception e) {
+			LOG.error(e.getMessage(), e);
+		}
+		return;
+	}
+
+	@Override
+	public void WebWxBatchGetContact() {
+		String url = String.format(URLEnum.WEB_WX_BATCH_GET_CONTACT.getUrl(),
+				core.getLoginInfo().get(StorageLoginInfoEnum.url.getKey()), new Date().getTime(),
+				core.getLoginInfo().get(StorageLoginInfoEnum.pass_ticket.getKey()));
+		Map<String, Object> paramMap = core.getParamMap();
+		paramMap.put("Count", core.getGroupIdList().size());
+		List<Map<String, String>> list = new ArrayList<Map<String, String>>();
+		for (int i = 0; i < core.getGroupIdList().size(); i++) {
+			HashMap<String, String> map = new HashMap<String, String>();
+			map.put("UserName", core.getGroupIdList().get(i));
+			map.put("EncryChatRoomId", "");
+			list.add(map);
+		}
+		paramMap.put("List", list);
+		HttpEntity entity = httpClient.doPost(url, JSON.toJSONString(paramMap));
+		try {
+			String text = EntityUtils.toString(entity, Consts.UTF_8);
+			JSONObject obj = JSON.parseObject(text);
+			JSONArray contactList = obj.getJSONArray("ContactList");
+			for (int i = 0; i < contactList.size(); i++) { // 群好友
+				if (contactList.getJSONObject(i).getString("UserName").indexOf("@@") > -1) { // 群
+					core.getGroupNickNameList().add(contactList.getJSONObject(i).getString("NickName")); // 更新群昵称列表
+					core.getGroupList().add(contactList.getJSONObject(i)); // 更新群信息(所有)列表
+					core.getGroupMemeberMap().put(contactList.getJSONObject(i).getString("UserName"),
+							contactList.getJSONObject(i).getJSONArray("MemberList")); // 更新群成员Map
+				}
+			}
+		} catch (Exception e) {
+			LOG.info(e.getMessage());
+		}
+	}
+
+	/**
+	 * 检查登陆状态
+	 *
+	 * @param result
+	 * @return
+	 */
+	public String checklogin(String result) {
+		String regEx = "window.code=(\\d+)";
+		Matcher matcher = CommonTools.getMatcher(regEx, result);
+		if (matcher.find()) {
+			return matcher.group(1);
+		}
+		return null;
+	}
+
+	/**
+	 * 处理登陆信息
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月9日 下午12:16:26
+	 * @param loginContent
+	 */
+	private void processLoginInfo(String loginContent) {
+		String regEx = "window.redirect_uri=\"(\\S+)\";";
+		Matcher matcher = CommonTools.getMatcher(regEx, loginContent);
+		if (matcher.find()) {
+			String originalUrl = matcher.group(1);
+			String url = originalUrl.substring(0, originalUrl.lastIndexOf('/')); // https://wx2.qq.com/cgi-bin/mmwebwx-bin
+			core.getLoginInfo().put("url", url);
+			Map<String, List<String>> possibleUrlMap = this.getPossibleUrlMap();
+			Iterator<Entry<String, List<String>>> iterator = possibleUrlMap.entrySet().iterator();
+			Map.Entry<String, List<String>> entry;
+			String fileUrl;
+			String syncUrl;
+			while (iterator.hasNext()) {
+				entry = iterator.next();
+				String indexUrl = entry.getKey();
+				fileUrl = "https://" + entry.getValue().get(0) + "/cgi-bin/mmwebwx-bin";
+				syncUrl = "https://" + entry.getValue().get(1) + "/cgi-bin/mmwebwx-bin";
+				if (core.getLoginInfo().get("url").toString().contains(indexUrl)) {
+					core.setIndexUrl(indexUrl);
+					core.getLoginInfo().put("fileUrl", fileUrl);
+					core.getLoginInfo().put("syncUrl", syncUrl);
+					break;
+				}
+			}
+			if (core.getLoginInfo().get("fileUrl") == null && core.getLoginInfo().get("syncUrl") == null) {
+				core.getLoginInfo().put("fileUrl", url);
+				core.getLoginInfo().put("syncUrl", url);
+			}
+			core.getLoginInfo().put("deviceid", "e" + String.valueOf(new Random().nextLong()).substring(1, 16)); // 生成15位随机数
+			core.getLoginInfo().put("BaseRequest", new ArrayList<String>());
+			String text = "";
+
+			try {
+				HttpEntity entity = myHttpClient.doGet(originalUrl, null, false, null);
+				text = EntityUtils.toString(entity);
+			} catch (Exception e) {
+				LOG.info(e.getMessage());
+				return;
+			}
+			//add by 默非默 2017-08-01 22:28:09
+			//如果登录被禁止时,则登录返回的message内容不为空,下面代码则判断登录内容是否为空,不为空则退出程序
+			String msg = getLoginMessage(text);
+			if (!"".equals(msg)){
+				LOG.info(msg);
+				System.exit(0);
+			}
+			Document doc = CommonTools.xmlParser(text);
+			if (doc != null) {
+				core.getLoginInfo().put(StorageLoginInfoEnum.skey.getKey(),
+						doc.getElementsByTagName(StorageLoginInfoEnum.skey.getKey()).item(0).getFirstChild()
+								.getNodeValue());
+				core.getLoginInfo().put(StorageLoginInfoEnum.wxsid.getKey(),
+						doc.getElementsByTagName(StorageLoginInfoEnum.wxsid.getKey()).item(0).getFirstChild()
+								.getNodeValue());
+				core.getLoginInfo().put(StorageLoginInfoEnum.wxuin.getKey(),
+						doc.getElementsByTagName(StorageLoginInfoEnum.wxuin.getKey()).item(0).getFirstChild()
+								.getNodeValue());
+				core.getLoginInfo().put(StorageLoginInfoEnum.pass_ticket.getKey(),
+						doc.getElementsByTagName(StorageLoginInfoEnum.pass_ticket.getKey()).item(0).getFirstChild()
+								.getNodeValue());
+			}
+
+		}
+	}
+
+	private Map<String, List<String>> getPossibleUrlMap() {
+		Map<String, List<String>> possibleUrlMap = new HashMap<String, List<String>>();
+		possibleUrlMap.put("wx.qq.com", new ArrayList<String>() {
+			/**
+			 *
+			 */
+			private static final long serialVersionUID = 1L;
+
+			{
+				add("file.wx.qq.com");
+				add("webpush.wx.qq.com");
+			}
+		});
+
+		possibleUrlMap.put("wx2.qq.com", new ArrayList<String>() {
+			/**
+			 *
+			 */
+			private static final long serialVersionUID = 1L;
+
+			{
+				add("file.wx2.qq.com");
+				add("webpush.wx2.qq.com");
+			}
+		});
+		possibleUrlMap.put("wx8.qq.com", new ArrayList<String>() {
+			/**
+			 *
+			 */
+			private static final long serialVersionUID = 1L;
+
+			{
+				add("file.wx8.qq.com");
+				add("webpush.wx8.qq.com");
+			}
+		});
+
+		possibleUrlMap.put("web2.wechat.com", new ArrayList<String>() {
+			/**
+			 *
+			 */
+			private static final long serialVersionUID = 1L;
+
+			{
+				add("file.web2.wechat.com");
+				add("webpush.web2.wechat.com");
+			}
+		});
+		possibleUrlMap.put("wechat.com", new ArrayList<String>() {
+			/**
+			 *
+			 */
+			private static final long serialVersionUID = 1L;
+
+			{
+				add("file.web.wechat.com");
+				add("webpush.web.wechat.com");
+			}
+		});
+		return possibleUrlMap;
+	}
+
+	/**
+	 * 同步消息 sync the messages
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月12日 上午12:24:55
+	 * @return
+	 */
+	private JSONObject webWxSync() {
+		JSONObject result = null;
+		String url = String.format(URLEnum.WEB_WX_SYNC_URL.getUrl(),
+				core.getLoginInfo().get(StorageLoginInfoEnum.url.getKey()),
+				core.getLoginInfo().get(StorageLoginInfoEnum.wxsid.getKey()),
+				core.getLoginInfo().get(StorageLoginInfoEnum.skey.getKey()),
+				core.getLoginInfo().get(StorageLoginInfoEnum.pass_ticket.getKey()));
+		Map<String, Object> paramMap = core.getParamMap();
+		paramMap.put(StorageLoginInfoEnum.SyncKey.getKey(),
+				core.getLoginInfo().get(StorageLoginInfoEnum.SyncKey.getKey()));
+		paramMap.put("rr", -new Date().getTime() / 1000);
+		String paramStr = JSON.toJSONString(paramMap);
+		try {
+			HttpEntity entity = myHttpClient.doPost(url, paramStr);
+			String text = EntityUtils.toString(entity, Consts.UTF_8);
+			JSONObject obj = JSON.parseObject(text);
+			if (obj.getJSONObject("BaseResponse").getInteger("Ret") != 0) {
+				result = null;
+			} else {
+				result = obj;
+				core.getLoginInfo().put(StorageLoginInfoEnum.SyncKey.getKey(), obj.getJSONObject("SyncCheckKey"));
+				JSONArray syncArray = obj.getJSONObject(StorageLoginInfoEnum.SyncKey.getKey()).getJSONArray("List");
+				StringBuilder sb = new StringBuilder();
+				for (int i = 0; i < syncArray.size(); i++) {
+					sb.append(syncArray.getJSONObject(i).getString("Key") + "_"
+							+ syncArray.getJSONObject(i).getString("Val") + "|");
+				}
+				String synckey = sb.toString();
+				core.getLoginInfo().put(StorageLoginInfoEnum.synckey.getKey(),
+						synckey.substring(0, synckey.length() - 1));// 1_656161336|2_656161626|3_656161313|11_656159955|13_656120033|201_1492273724|1000_1492265953|1001_1492250432|1004_1491805192
+			}
+		} catch (Exception e) {
+			LOG.info(e.getMessage());
+		}
+		return result;
+
+	}
+
+	/**
+	 * 检查是否有新消息 check whether there's a message
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月16日 上午11:11:34
+	 * @return
+	 *
+	 */
+	private Map<String, String> syncCheck() {
+		Map<String, String> resultMap = new HashMap<String, String>();
+		// 组装请求URL和参数
+		String url = core.getLoginInfo().get(StorageLoginInfoEnum.syncUrl.getKey()) + URLEnum.SYNC_CHECK_URL.getUrl();
+		List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
+		for (BaseParaEnum baseRequest : BaseParaEnum.values()) {
+			params.add(new BasicNameValuePair(baseRequest.para().toLowerCase(),
+					core.getLoginInfo().get(baseRequest.value()).toString()));
+		}
+		params.add(new BasicNameValuePair("r", String.valueOf(new Date().getTime())));
+		params.add(new BasicNameValuePair("synckey", (String) core.getLoginInfo().get("synckey")));
+		params.add(new BasicNameValuePair("_", String.valueOf(new Date().getTime())));
+		SleepUtils.sleep(7);
+		try {
+			HttpEntity entity = myHttpClient.doGet(url, params, true, null);
+			if (entity == null) {
+				resultMap.put("retcode", "9999");
+				resultMap.put("selector", "9999");
+				return resultMap;
+			}
+			String text = EntityUtils.toString(entity);
+			String regEx = "window.synccheck=\\{retcode:\"(\\d+)\",selector:\"(\\d+)\"\\}";
+			Matcher matcher = CommonTools.getMatcher(regEx, text);
+			if (!matcher.find() || matcher.group(1).equals("2")) {
+				LOG.info(String.format("Unexpected sync check result: %s", text));
+			} else {
+				resultMap.put("retcode", matcher.group(1));
+				resultMap.put("selector", matcher.group(2));
+			}
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return resultMap;
+	}
+
+	/**
+	 * 解析登录返回的消息,如果成功登录,则message为空
+	 * @param result
+	 * @return
+	 */
+	public String getLoginMessage(String result){
+		String[] strArr = result.split("<message>");
+		String[] rs = strArr[1].split("</message>");
+		if (rs!=null && rs.length>1) {
+			return rs[0];
+		}
+		return "";
+	}
+}

+ 38 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/thread/CheckLoginStatusThread.java

@@ -0,0 +1,38 @@
+package com.xmzs.common.wechat.thread;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.xmzs.common.wechat.core.Core;
+import com.xmzs.common.wechat.utils.SleepUtils;
+
+/**
+ * 检查微信在线状态
+ * <p>
+ * 如何来感知微信状态?
+ * 微信会有心跳包,LoginServiceImpl.syncCheck()正常在线情况下返回的消息中retcode报文应该为"0",心跳间隔一般在25秒,
+ * 那么可以通过最后收到正常报文的时间来作为判断是否在线的依据。若报文间隔大于60秒,则认为已掉线。
+ * </p>
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年5月17日 下午10:53:15
+ * @version 1.0
+ *
+ */
+public class CheckLoginStatusThread implements Runnable {
+	private static Logger LOG = LoggerFactory.getLogger(CheckLoginStatusThread.class);
+	private Core core = Core.getInstance();
+
+	@Override
+	public void run() {
+		while (core.isAlive()) {
+			long t1 = System.currentTimeMillis(); // 秒为单位
+			if (t1 - core.getLastNormalRetcodeTime() > 60 * 1000) { // 超过60秒,判为离线
+				core.setAlive(false);
+				LOG.info("微信已离线");
+			}
+			SleepUtils.sleep(10 * 1000); // 休眠10秒
+		}
+	}
+
+}

+ 81 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/Config.java

@@ -0,0 +1,81 @@
+package com.xmzs.common.wechat.utils;
+
+import com.xmzs.common.wechat.utils.enums.OsNameEnum;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+
+/**
+ * 配置信息
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年4月23日 下午2:26:21
+ * @version 1.0
+ *
+ */
+public class Config {
+
+	public static final String API_WXAPPID = "API_WXAPPID";
+
+	public static final String picDir = "D://itchat4j";
+	public static final String VERSION = "1.4.1";
+	public static final String BASE_URL = "https://login.weixin.qq.com";
+	public static final String REFERER = "https://wx.qq.com/?&lang=zh_CN&target=t";
+	public static final String OS = "";
+	public static final String DIR = "";
+	public static final String DEFAULT_QR = "QR.jpg";
+	public static final String USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36";
+	public static final String UOS_PATCH_CLIENT_VERSION = "2.0.0";
+
+	public static final String UOS_PATCH_EXTSPAM =
+			"Go8FCIkFEokFCggwMDAwMDAwMRAGGvAESySibk50w5Wb3uTl2c2h64jVVrV7gNs06GFlWplHQbY/5FfiO++1yH4ykCyNPWKXmco+wfQzK5R98D3so7rJ5LmGFvBLjGceleySrc3SOf2Pc1gVehzJgODeS0lDL3/I/0S2SSE98YgKleq6Uqx6ndTy9yaL9qFxJL7eiA/R3SEfTaW1SBoSITIu+EEkXff+Pv8NHOk7N57rcGk1w0ZzRrQDkXTOXFN2iHYIzAAZPIOY45Lsh+A4slpgnDiaOvRtlQYCt97nmPLuTipOJ8Qc5pM7ZsOsAPPrCQL7nK0I7aPrFDF0q4ziUUKettzW8MrAaiVfmbD1/VkmLNVqqZVvBCtRblXb5FHmtS8FxnqCzYP4WFvz3T0TcrOqwLX1M/DQvcHaGGw0B0y4bZMs7lVScGBFxMj3vbFi2SRKbKhaitxHfYHAOAa0X7/MSS0RNAjdwoyGHeOepXOKY+h3iHeqCvgOH6LOifdHf/1aaZNwSkGotYnYScW8Yx63LnSwba7+hESrtPa/huRmB9KWvMCKbDThL/nne14hnL277EDCSocPu3rOSYjuB9gKSOdVmWsj9Dxb/iZIe+S6AiG29Esm+/eUacSba0k8wn5HhHg9d4tIcixrxveflc8vi2/wNQGVFNsGO6tB5WF0xf/plngOvQ1/ivGV/C1Qpdhzznh0ExAVJ6dwzNg7qIEBaw+BzTJTUuRcPk92Sn6QDn2Pu3mpONaEumacjW4w6ipPnPw+g2TfywJjeEcpSZaP4Q3YV5HG8D6UjWA4GSkBKculWpdCMadx0usMomsSS/74QgpYqcPkmamB4nVv1JxczYITIqItIKjD35IGKAUwAA==";
+
+
+	public static final ArrayList<String> API_SPECIAL_USER = new ArrayList<String>(Arrays.asList("filehelper", "weibo",
+			"qqmail", "fmessage", "tmessage", "qmessage", "qqsync", "floatbottle", "lbsapp", "shakeapp", "medianote",
+			"qqfriend", "readerapp", "blogapp", "facebookapp", "masssendapp", "meishiapp", "feedsapp", "voip",
+			"blogappweixin", "brandsessionholder", "weixin", "weixinreminder", "officialaccounts", "wxitil",
+			"notification_messages", "wxid_novlwrv3lqwv11", "gh_22b87fa7cb3c", "userexperience_alarm"));
+
+	/**
+	 * 获取文件目录
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月8日 下午10:27:42
+	 * @return
+	 */
+	public static String getLocalPath() {
+		String localPath = null;
+		try {
+			localPath = new File("").getCanonicalPath();
+		} catch (IOException e) {
+			// TODO Auto-generated catch block
+			e.printStackTrace();
+		}
+		return localPath;
+	}
+
+	/**
+	 * 获取系统平台
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月8日 下午10:27:53
+	 */
+	public static OsNameEnum getOsNameEnum() {
+		String os = System.getProperty("os.name").toUpperCase();
+		if (os.indexOf(OsNameEnum.DARWIN.toString()) >= 0) {
+			return OsNameEnum.DARWIN;
+		} else if (os.indexOf(OsNameEnum.WINDOWS.toString()) >= 0) {
+			return OsNameEnum.WINDOWS;
+		} else if (os.indexOf(OsNameEnum.LINUX.toString()) >= 0) {
+			return OsNameEnum.LINUX;
+		} else if (os.indexOf(OsNameEnum.MAC.toString()) >= 0) {
+			return OsNameEnum.MAC;
+		}
+		return OsNameEnum.OTHER;
+	}
+
+}

+ 34 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/ConstantConfigEnum.java

@@ -0,0 +1,34 @@
+package com.xmzs.common.wechat.utils;
+
+/**
+ * 常量
+ *
+ * @author https=//github.com/yaphone
+ * @date 创建时间:2017年5月5日 下午11=29=04
+ * @version 1.0
+ *
+ */
+public class ConstantConfigEnum {
+	public static final int APPMSGTYPE_TEXT = 1;
+	public static final int APPMSGTYPE_IMG = 2;
+	public static final int APPMSGTYPE_AUDIO = 3;
+	public static final int APPMSGTYPE_VIDEO = 4;
+	public static final int APPMSGTYPE_URL = 5;
+	public static final int APPMSGTYPE_ATTACH = 6;
+	public static final int APPMSGTYPE_OPEN = 7;
+	public static final int APPMSGTYPE_EMOJI = 8;
+	public static final int APPMSGTYPE_VOICE_REMIND = 9;
+	public static final int APPMSGTYPE_SCAN_GOOD = 10;
+	public static final int APPMSGTYPE_GOOD = 13;
+	public static final int APPMSGTYPE_EMOTION = 15;
+	public static final int APPMSGTYPE_CARD_TICKET = 16;
+	public static final int APPMSGTYPE_REALTIME_SHARE_LOCATION = 17;
+	// public static final int APPMSGTYPE_TRANSFERS = 2e3;
+	public static final int APPMSGTYPE_RED_ENVELOPES = 2001;
+	public static final int APPMSGTYPE_READER_TYPE = 100001;
+	public static final int UPLOAD_MEDIA_TYPE_IMAGE = 1;
+	public static final int UPLOAD_MEDIA_TYPE_VIDEO = 2;
+	public static final int UPLOAD_MEDIA_TYPE_AUDIO = 3;
+	public static final int UPLOAD_MEDIA_TYPE_ATTACHMENT = 4;
+
+}

+ 6 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/MsgKeywords.java

@@ -0,0 +1,6 @@
+package com.xmzs.common.wechat.utils;
+
+public class MsgKeywords {
+	public static String newFriendStr = "我通过了你的朋友验证请求";
+
+}

+ 194 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/MyHttpClient.java

@@ -0,0 +1,194 @@
+package com.xmzs.common.wechat.utils;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import org.apache.http.Consts;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.CookieStore;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.cookie.Cookie;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+
+/**
+ * HTTP访问类,对Apache HttpClient进行简单封装,适配器模式
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年4月9日 下午7:05:04
+ * @version 1.0
+ *
+ */
+public class MyHttpClient {
+	private Logger logger = Logger.getLogger("MyHttpClient");
+
+	private static CloseableHttpClient httpClient = HttpClients.createDefault();
+
+	private static MyHttpClient instance = null;
+
+	private static CookieStore cookieStore;
+
+	static {
+		cookieStore = new BasicCookieStore();
+
+		// 将CookieStore设置到httpClient中
+		httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
+	}
+
+	public static String getCookie(String name) {
+		List<Cookie> cookies = cookieStore.getCookies();
+		for (Cookie cookie : cookies) {
+			if (cookie.getName().equalsIgnoreCase(name)) {
+				return cookie.getValue();
+			}
+		}
+		return null;
+
+	}
+
+	private MyHttpClient() {
+
+	}
+
+	/**
+	 * 获取cookies
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月7日 下午8:37:17
+	 * @return
+	 */
+	public static MyHttpClient getInstance() {
+		if (instance == null) {
+			synchronized (MyHttpClient.class) {
+				if (instance == null) {
+					instance = new MyHttpClient();
+				}
+			}
+		}
+		return instance;
+	}
+
+	/**
+	 * 处理GET请求
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月9日 下午7:06:19
+	 * @param url
+	 * @param params
+	 * @return
+	 */
+	public HttpEntity doGet(String url, List<BasicNameValuePair> params, boolean redirect,
+			Map<String, String> headerMap) {
+		HttpEntity entity = null;
+		HttpGet httpGet = new HttpGet();
+
+		try {
+			if (params != null) {
+				String paramStr = EntityUtils.toString(new UrlEncodedFormEntity(params, Consts.UTF_8));
+				httpGet = new HttpGet(url + "?" + paramStr);
+			} else {
+				httpGet = new HttpGet(url);
+			}
+			if (!redirect) {
+				httpGet.setConfig(RequestConfig.custom().setRedirectsEnabled(false).build()); // 禁止重定向
+			}
+			httpGet.setHeader("User-Agent", Config.USER_AGENT);
+			httpGet.setHeader("client-version", Config.UOS_PATCH_CLIENT_VERSION);
+			httpGet.setHeader("extspam", Config.UOS_PATCH_EXTSPAM);
+			httpGet.setHeader("referer", Config.REFERER);
+			if (headerMap != null) {
+				Set<Entry<String, String>> entries = headerMap.entrySet();
+				for (Entry<String, String> entry : entries) {
+					httpGet.setHeader(entry.getKey(), entry.getValue());
+				}
+			}
+			CloseableHttpResponse response = httpClient.execute(httpGet);
+			entity = response.getEntity();
+		} catch (ClientProtocolException e) {
+			logger.info(e.getMessage());
+		} catch (IOException e) {
+			logger.info(e.getMessage());
+		}
+
+		return entity;
+	}
+
+	/**
+	 * 处理POST请求
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月9日 下午7:06:35
+	 * @param url
+	 * @param params
+	 * @return
+	 */
+	public HttpEntity doPost(String url, String paramsStr) {
+		HttpEntity entity = null;
+		HttpPost httpPost = new HttpPost();
+		try {
+			StringEntity params = new StringEntity(paramsStr, Consts.UTF_8);
+			httpPost = new HttpPost(url);
+			httpPost.setEntity(params);
+			httpPost.setHeader("Content-type", "application/json; charset=utf-8");
+			httpPost.setHeader("User-Agent", Config.USER_AGENT);
+			httpPost.setHeader("client-version", Config.UOS_PATCH_CLIENT_VERSION);
+			httpPost.setHeader("extspam", Config.UOS_PATCH_EXTSPAM);
+			httpPost.setHeader("referer", Config.REFERER);
+
+			CloseableHttpResponse response = httpClient.execute(httpPost);
+			entity = response.getEntity();
+		} catch (ClientProtocolException e) {
+			logger.info(e.getMessage());
+		} catch (IOException e) {
+			logger.info(e.getMessage());
+		}
+
+		return entity;
+	}
+
+	/**
+	 * 上传文件到服务器
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年5月7日 下午9:19:23
+	 * @param url
+	 * @param reqEntity
+	 * @return
+	 */
+	public HttpEntity doPostFile(String url, HttpEntity reqEntity) {
+		HttpEntity entity = null;
+		HttpPost httpPost = new HttpPost(url);
+		httpPost.setHeader("User-Agent", Config.USER_AGENT);
+		httpPost.setHeader("client-version", Config.UOS_PATCH_CLIENT_VERSION);
+		httpPost.setHeader("extspam", Config.UOS_PATCH_EXTSPAM);
+		httpPost.setHeader("referer", Config.REFERER);
+
+		httpPost.setEntity(reqEntity);
+		try {
+			CloseableHttpResponse response = httpClient.execute(httpPost);
+			entity = response.getEntity();
+
+		} catch (Exception e) {
+			logger.info(e.getMessage());
+		}
+		return entity;
+	}
+
+	public static CloseableHttpClient getHttpClient() {
+		return httpClient;
+	}
+
+}

+ 20 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/SleepUtils.java

@@ -0,0 +1,20 @@
+package com.xmzs.common.wechat.utils;
+
+/**
+ * Created by xiaoxiaomo on 2017/5/6.
+ */
+public class SleepUtils {
+
+    /**
+     * 毫秒为单位
+     * @param time
+     */
+    public static void sleep( long time ){
+        try {
+            Thread.sleep( time );
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+
+}

+ 68 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/MsgCodeEnum.java

@@ -0,0 +1,68 @@
+package com.xmzs.common.wechat.utils.enums;
+
+/**
+ * 消息类型
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年4月23日 下午12:15:00
+ * @version 1.0
+ *
+ */
+public enum MsgCodeEnum {
+
+	// public static final int MSGTYPE_TEXT = 1; // 文本消息类型
+	// public static final int MSGTYPE_IMAGE = 3; // 图片消息
+	// public static final int MSGTYPE_VOICE = 34; // 语音消息
+	// public static final int MSGTYPE_VIDEO = 43; // 小视频消息
+	// public static final int MSGTYPE_MICROVIDEO = 62; // 短视频消息
+	// public static final int MSGTYPE_EMOTICON = 47; // 表情消息
+	// public static final int MSGTYPE_APP = 49;
+	// public static final int MSGTYPE_VOIPMSG = 50;
+	// public static final int MSGTYPE_VOIPNOTIFY = 52;
+	// public static final int MSGTYPE_VOIPINVITE = 53;
+	// public static final int MSGTYPE_LOCATION = 48;
+	// public static final int MSGTYPE_STATUSNOTIFY = 51;
+	// public static final int MSGTYPE_SYSNOTICE = 9999;
+	// public static final int MSGTYPE_POSSIBLEFRIEND_MSG = 40;
+	// public static final int MSGTYPE_VERIFYMSG = 37;
+	// public static final int MSGTYPE_SHARECARD = 42;
+	// public static final int MSGTYPE_SYS = 10000;
+	// public static final int MSGTYPE_RECALLED = 10002;
+	MSGTYPE_TEXT(1, "文本消息类型"),
+	MSGTYPE_IMAGE(3, "图片消息"),
+	MSGTYPE_VOICE(34, "语音消息"),
+	MSGTYPE_VIDEO(43, "小视频消息"),
+	MSGTYPE_MICROVIDEO(62, "短视频消息"),
+	MSGTYPE_EMOTICON(47, "表情消息"),
+	MSGTYPE_MEDIA(49, "多媒体消息"),
+	MSGTYPE_VOIPMSG(50, ""),
+	MSGTYPE_VOIPNOTIFY(52, ""),
+	MSGTYPE_VOIPINVITE(53, ""),
+	MSGTYPE_LOCATION(48, ""),
+	MSGTYPE_STATUSNOTIFY(51, ""),
+	MSGTYPE_SYSNOTICE(9999, ""),
+	MSGTYPE_POSSIBLEFRIEND_MSG(40, ""),
+	MSGTYPE_VERIFYMSG(37, "好友请求"),
+	MSGTYPE_SHARECARD(42, ""),
+	MSGTYPE_SYS(10000, "系统消息"),
+	MSGTYPE_RECALLED(10002, "")
+
+	;
+
+	private int code;
+	private String type;
+
+	MsgCodeEnum(int code, String type) {
+		this.code = code;
+		this.type = type;
+	}
+
+	public int getCode() {
+		return code;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+}

+ 38 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/MsgTypeEnum.java

@@ -0,0 +1,38 @@
+package com.xmzs.common.wechat.utils.enums;
+
+
+/**
+ * 消息类型枚举类
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年5月13日 下午11:53:00
+ * @version 1.0
+ *
+ */
+public enum MsgTypeEnum {
+	TEXT("Text", "文本消息"),
+	PIC("Pic", "图片消息"),
+	VOICE("Voice", "语音消息"),
+	VIEDO("Viedo", "小视频消息"),
+	NAMECARD("NameCard", "名片消息"),
+	SYS("Sys", "系统消息"),
+	VERIFYMSG("VerifyMsg", "添加好友"),
+	MEDIA("app", "文件消息");
+
+	private String type;
+	private String code;
+
+	MsgTypeEnum(String type, String code) {
+		this.type = type;
+		this.code = code;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+}

+ 13 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/OsNameEnum.java

@@ -0,0 +1,13 @@
+package com.xmzs.common.wechat.utils.enums;
+
+/**
+ * 系统平台
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年4月8日 下午10:36:28
+ * @version 1.0
+ *
+ */
+public enum OsNameEnum {
+	WINDOWS, LINUX, DARWIN, MAC, OTHER
+}

+ 35 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/ResultEnum.java

@@ -0,0 +1,35 @@
+package com.xmzs.common.wechat.utils.enums;
+
+/**
+ * 返回结构枚举类
+ * <p>
+ * Created by xiaoxiaomo on 2017/5/6.
+ */
+public enum ResultEnum {
+
+    SUCCESS("200", "成功"),
+    WAIT_CONFIRM("201", "请在手机上点击确认"),
+    WAIT_SCAN("400", "请扫描二维码");
+
+    private String code;
+    private String msg;
+
+    ResultEnum(String code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+//    public static MsgInfoEnum getCode(String code) {
+//        switch (code) {
+//            case "Text":
+//                return MsgInfoEnum.TEXT;
+//            default:
+//                return MsgInfoEnum.VIDEO;
+//        }
+//    }
+
+}

+ 30 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/RetCodeEnum.java

@@ -0,0 +1,30 @@
+package com.xmzs.common.wechat.utils.enums;
+
+public enum RetCodeEnum {
+
+	NORMAL("0", "普通"),
+	LOGIN_OUT("1102", "退出"),
+	LOGIN_OTHERWHERE("1101", "其它地方登陆"),
+	MOBILE_LOGIN_OUT("1102", "移动端退出"),
+	UNKOWN("9999", "未知")
+
+	;
+
+
+	private String code;
+	private String type;
+
+	RetCodeEnum(String code, String type) {
+		this.code = code;
+		this.type = type;
+	}
+
+	public String getCode() {
+		return code;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+}

+ 59 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/StorageLoginInfoEnum.java

@@ -0,0 +1,59 @@
+package com.xmzs.common.wechat.utils.enums;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by xiaoxiaomo on 2017/5/7.
+ */
+public enum StorageLoginInfoEnum {
+
+    //URL
+    url("url",new String()),
+    fileUrl("fileUrl",new String()),
+    syncUrl("syncUrl",new String()),
+
+    deviceid("deviceid",new String()), //生成15位随机数
+
+    //baseRequest
+    skey("skey",new String()),
+    wxsid("wxsid",new String()),
+    wxuin("wxuin",new String()),
+    pass_ticket("pass_ticket",new String()),
+
+
+    InviteStartCount("InviteStartCount",0),
+    User("User",new JSONObject()),
+    SyncKey("SyncKey",new JSONObject()),
+    synckey("synckey",new String()),
+
+
+
+    MemberCount("MemberCount",new String()),
+    MemberList("MemberList",new JSONArray()),
+
+
+
+    ;
+
+    private String key;
+    private Object type;
+
+    StorageLoginInfoEnum(String key, Object type) {
+        this.key = key;
+        this.type = type;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+
+    public Object getType() {
+        return type;
+    }
+
+}

+ 49 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/URLEnum.java

@@ -0,0 +1,49 @@
+package com.xmzs.common.wechat.utils.enums;
+
+/**
+ * URL
+ * Created by xiaoxiaomo on 2017/5/6.
+ */
+public enum URLEnum {
+
+
+
+    BASE_URL("https://login.weixin.qq.com","基本的URL"),
+    UUID_URL(BASE_URL.url+"/jslogin","UUIDLURL"),
+    QRCODE_URL(BASE_URL.url+"/qrcode/","初始化URL"),
+    STATUS_NOTIFY_URL(BASE_URL.url+"/webwxstatusnotify?lang=zh_CN&pass_ticket=%s","微信状态通知"),
+    LOGIN_URL(BASE_URL.url+"/cgi-bin/mmwebwx-bin/login","登陆URL"),
+    INIT_URL("%s/webwxinit?r=%s&pass_ticket=%s","初始化URL"),
+    SYNC_CHECK_URL("/synccheck","检查心跳URL"),
+    WEB_WX_SYNC_URL("%s/webwxsync?sid=%s&skey=%s&pass_ticket=%s","web微信消息同步URL"),
+    WEB_WX_GET_CONTACT("%s/webwxgetcontact","web微信获取联系人信息URL"),
+    WEB_WX_SEND_MSG("%s/webwxsendmsg","发送消息URL"),
+    WEB_WX_UPLOAD_MEDIA("%s/webwxuploadmedia?f=json", "上传文件到服务器"),
+    WEB_WX_GET_MSG_IMG("%s/webwxgetmsgimg", "下载图片消息"),
+    WEB_WX_GET_VOICE("%s/webwxgetvoice", "下载语音消息"),
+    WEB_WX_GET_VIEDO("%s/webwxgetvideo", "下载语音消息"),
+    WEB_WX_PUSH_LOGIN("%s/webwxpushloginurl", "不扫码登陆"),
+    WEB_WX_LOGOUT("%s/webwxlogout", "退出微信"),
+    WEB_WX_BATCH_GET_CONTACT("%s/webwxbatchgetcontact?type=ex&r=%s&lang=zh_CN&pass_ticket=%s", "查询群信息"),
+	WEB_WX_REMARKNAME("%s/webwxoplog?lang=zh_CN&pass_ticket=%s", "修改好友备注"),
+    WEB_WX_VERIFYUSER("%s/webwxverifyuser?r=%s&lang=zh_CN&pass_ticket=%s", "被动添加好友"),
+    WEB_WX_GET_MEDIA("%s/webwxgetmedia", "下载文件")
+
+
+
+
+    ;
+
+    private String url;
+    private String msg;
+
+    URLEnum(String url, String msg) {
+        this.url = url;
+        this.msg = msg;
+    }
+
+
+    public String getUrl() {
+        return url;
+    }
+}

+ 28 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/VerifyFriendEnum.java

@@ -0,0 +1,28 @@
+package com.xmzs.common.wechat.utils.enums;
+
+/**
+ * 确认添加好友Enum
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年6月29日 下午9:47:14
+ * @version 1.0
+ *
+ */
+public enum VerifyFriendEnum {
+
+	ADD(2, "添加"),
+	ACCEPT(3, "接受");
+
+	private int code;
+	private String desc;
+
+	private VerifyFriendEnum(int code, String desc) {
+		this.code = code;
+		this.desc = desc;
+	}
+
+	public int getCode() {
+		return code;
+	}
+
+}

+ 36 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/parameters/BaseParaEnum.java

@@ -0,0 +1,36 @@
+package com.xmzs.common.wechat.utils.enums.parameters;
+
+/**
+ *
+ * 基本请求参数
+ * 1. webWxInit      初始化
+ * 2. wxStatusNotify 微信状态通知
+ *
+ * <p>
+ * Created by xiaoxiaomo on 2017/5/7.
+ */
+public enum BaseParaEnum {
+
+    Uin("Uin", "wxuin"),
+    Sid("Sid", "wxsid"),
+    Skey("Skey", "skey"),
+    DeviceID("DeviceID", "pass_ticket");
+
+    private String para;
+    private String value;
+
+    BaseParaEnum(String para, String value) {
+        this.para = para;
+        this.value = value;
+    }
+
+    public String para() {
+        return para;
+    }
+
+
+    public Object value() {
+        return value;
+    }
+
+}

+ 31 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/parameters/LoginParaEnum.java

@@ -0,0 +1,31 @@
+package com.xmzs.common.wechat.utils.enums.parameters;
+
+/**
+ * 登陆
+ * <p>
+ * Created by xiaoxiaomo on 2017/5/7.
+ */
+public enum LoginParaEnum {
+
+    LOGIN_ICON("loginicon", "true"),
+    UUID("uuid", ""),
+    TIP("tip", "0"),
+    R("r", ""),
+    _1("_", "");
+
+    private String para;
+    private String value;
+
+    LoginParaEnum(String para, String value) {
+        this.para = para;
+        this.value = value;
+    }
+
+    public String para() {
+        return para;
+    }
+
+    public String value() {
+        return value;
+    }
+}

+ 30 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/parameters/StatusNotifyParaEnum.java

@@ -0,0 +1,30 @@
+package com.xmzs.common.wechat.utils.enums.parameters;
+
+/**
+ * 状态通知
+ * <p>
+ * Created by xiaoxiaomo on 2017/5/7.
+ */
+public enum StatusNotifyParaEnum {
+
+    CODE("Code", "3"),
+    FROM_USERNAME("FromUserName", ""),
+    TO_USERNAME("ToUserName", ""),
+    CLIENT_MSG_ID("ClientMsgId", ""); //时间戳
+
+    private String para;
+    private String value;
+
+    StatusNotifyParaEnum(String para, String value) {
+        this.para = para;
+        this.value = value;
+    }
+
+    public String para() {
+        return para;
+    }
+
+    public String value() {
+        return value;
+    }
+}

+ 33 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/enums/parameters/UUIDParaEnum.java

@@ -0,0 +1,33 @@
+package com.xmzs.common.wechat.utils.enums.parameters;
+
+/**
+ * UUID
+ * <p>
+ * Created by xiaoxiaomo on 2017/5/7.
+ */
+public enum UUIDParaEnum {
+
+    APP_ID("appid", "wx782c26e4c19acffb"),
+    FUN("fun", "new"),
+    LANG("lang", "zh_CN"),
+    REDIRECT_URL("redirect_uri",
+            "https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?mod=desktop"),
+    _1("_", "时间戳");
+
+
+    private String para;
+    private String value;
+
+    UUIDParaEnum(String para, String value) {
+        this.para = para;
+        this.value = value;
+    }
+
+    public String para() {
+        return para;
+    }
+
+    public String value() {
+        return value;
+    }
+}

+ 243 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/tools/CommonTools.java

@@ -0,0 +1,243 @@
+package com.xmzs.common.wechat.utils.tools;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.vdurmont.emoji.EmojiParser;
+
+import com.xmzs.common.wechat.utils.Config;
+import com.xmzs.common.wechat.utils.enums.OsNameEnum;
+
+/**
+ * 常用工具类
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年4月8日 下午10:59:55
+ * @version 1.0
+ *
+ */
+public class CommonTools {
+
+	public static boolean printQr(String qrPath) {
+
+		switch (Config.getOsNameEnum()) {
+		case WINDOWS:
+			if (Config.getOsNameEnum().equals(OsNameEnum.WINDOWS)) {
+				Runtime runtime = Runtime.getRuntime();
+				try {
+					runtime.exec("cmd /c start " + qrPath);
+				} catch (Exception e) {
+					e.printStackTrace();
+				}
+			}
+			break;
+		case MAC:
+			if (Config.getOsNameEnum().equals(OsNameEnum.MAC)) {
+				Runtime runtime = Runtime.getRuntime();
+				try {
+					runtime.exec("open " + qrPath);
+				} catch (Exception e) {
+					e.printStackTrace();
+				}
+			}
+			break;
+
+		default:
+			break;
+		}
+		return true;
+	}
+
+	public static boolean clearScreen() {
+		switch (Config.getOsNameEnum()) {
+		case WINDOWS:
+			if (Config.getOsNameEnum().equals(OsNameEnum.WINDOWS)) {
+				Runtime runtime = Runtime.getRuntime();
+				try {
+					runtime.exec("cmd /c " + "cls");
+				} catch (Exception e) {
+					e.printStackTrace();
+				}
+			}
+			break;
+
+		default:
+			break;
+		}
+		return true;
+	}
+
+	/**
+	 * 正则表达式处理工具
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月9日 上午12:27:10
+	 * @return
+	 */
+	public static Matcher getMatcher(String regEx, String text) {
+		Pattern pattern = Pattern.compile(regEx);
+		Matcher matcher = pattern.matcher(text);
+		return matcher;
+	}
+
+	/**
+	 * xml解析器
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月9日 下午6:24:25
+	 * @param text
+	 * @return
+	 */
+	public static Document xmlParser(String text) {
+		Document doc = null;
+		StringReader sr = new StringReader(text);
+		InputSource is = new InputSource(sr);
+		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+		try {
+			DocumentBuilder builder = factory.newDocumentBuilder();
+			doc = builder.parse(is);
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+		return doc;
+	}
+
+	public static JSONObject structFriendInfo(JSONObject userObj) {
+		Map<String, Object> friendInfoTemplate = new HashMap<String, Object>();
+		friendInfoTemplate.put("UserName", "");
+		friendInfoTemplate.put("City", "");
+		friendInfoTemplate.put("DisplayName", "");
+		friendInfoTemplate.put("PYQuanPin", "");
+		friendInfoTemplate.put("RemarkPYInitial", "");
+		friendInfoTemplate.put("Province", "");
+		friendInfoTemplate.put("KeyWord", "");
+		friendInfoTemplate.put("RemarkName", "");
+		friendInfoTemplate.put("PYInitial", "");
+		friendInfoTemplate.put("EncryChatRoomId", "");
+		friendInfoTemplate.put("Alias", "");
+		friendInfoTemplate.put("Signature", "");
+		friendInfoTemplate.put("NickName", "");
+		friendInfoTemplate.put("RemarkPYQuanPin", "");
+		friendInfoTemplate.put("HeadImgUrl", "");
+
+		friendInfoTemplate.put("UniFriend", 0);
+		friendInfoTemplate.put("Sex", 0);
+		friendInfoTemplate.put("AppAccountFlag", 0);
+		friendInfoTemplate.put("VerifyFlag", 0);
+		friendInfoTemplate.put("ChatRoomId", 0);
+		friendInfoTemplate.put("HideInputBarFlag", 0);
+		friendInfoTemplate.put("AttrStatus", 0);
+		friendInfoTemplate.put("SnsFlag", 0);
+		friendInfoTemplate.put("MemberCount", 0);
+		friendInfoTemplate.put("OwnerUin", 0);
+		friendInfoTemplate.put("ContactFlag", 0);
+		friendInfoTemplate.put("Uin", 0);
+		friendInfoTemplate.put("StarFriend", 0);
+		friendInfoTemplate.put("Statues", 0);
+
+		friendInfoTemplate.put("MemberList", new ArrayList<Object>());
+
+		JSONObject r = new JSONObject();
+		Set<String> keySet = friendInfoTemplate.keySet();
+		for (String key : keySet) {
+			if (userObj.containsKey(key)) {
+				r.put(key, userObj.get(key));
+			} else {
+				r.put(key, friendInfoTemplate.get(key));
+			}
+		}
+
+		return r;
+	}
+
+	public static String getSynckey(JSONObject obj) {
+		JSONArray obj2 = obj.getJSONArray("List");
+		StringBuilder sb = new StringBuilder();
+		for (int i = 0; i < obj2.size(); i++) {
+			JSONObject obj3 = (JSONObject) JSON.toJSON(obj2.get(i));
+			sb.append(obj3.get("Val") + "|");
+		}
+		return sb.substring(0, sb.length() - 1); // 656159784|656159911|656159873|1491905341
+
+	}
+
+	public static JSONObject searchDictList(List<JSONObject> list, String key, String value) {
+		JSONObject r = null;
+		for (JSONObject i : list) {
+			if (i.getString(key).equals(value)) {
+				r = i;
+				break;
+			}
+		}
+		return r;
+	}
+
+	/**
+	 * 处理emoji表情
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月23日 下午2:39:04
+	 * @param d
+	 * @param k
+	 */
+	public static void emojiFormatter(JSONObject d, String k) {
+		Matcher matcher = getMatcher("<span class=\"emoji emoji(.{1,10})\"></span>", d.getString(k));
+		StringBuilder sb = new StringBuilder();
+		String content = d.getString(k);
+		int lastStart = 0;
+		while (matcher.find()) {
+			String str = matcher.group(1);
+			if (str.length() == 6) {
+
+			} else if (str.length() == 10) {
+
+			} else {
+				str = "&#x" + str + ";";
+				String tmp = content.substring(lastStart, matcher.start());
+				sb.append(tmp + str);
+				lastStart = matcher.end();
+			}
+		}
+		if (lastStart < content.length()) {
+			sb.append(content.substring(lastStart));
+		}
+		if (sb.length() != 0) {
+			d.put(k, EmojiParser.parseToUnicode(sb.toString()));
+		} else {
+			d.put(k, content);
+		}
+
+	}
+
+	/**
+	 * 消息格式化
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月23日 下午4:19:08
+	 * @param d
+	 * @param k
+	 */
+	public static void msgFormatter(JSONObject d, String k) {
+		d.put(k, d.getString(k).replace("<br/>", "\n"));
+		emojiFormatter(d, k);
+		// TODO 与emoji表情有部分兼容问题,目前暂未处理解码处理 d.put(k,
+		// StringEscapeUtils.unescapeHtml4(d.getString(k)));
+
+	}
+
+}

+ 80 - 0
ruoyi-common/ruoyi-common-wechat/src/main/java/com/xmzs/common/wechat/utils/tools/DownloadTools.java

@@ -0,0 +1,80 @@
+package com.xmzs.common.wechat.utils.tools;
+
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+
+import com.xmzs.common.wechat.beans.BaseMsg;
+import com.xmzs.common.wechat.core.Core;
+import com.xmzs.common.wechat.utils.MyHttpClient;
+import com.xmzs.common.wechat.utils.enums.MsgTypeEnum;
+import com.xmzs.common.wechat.utils.enums.URLEnum;
+
+/**
+ * 下载工具类
+ *
+ * @author https://github.com/yaphone
+ * @date 创建时间:2017年4月21日 下午11:18:46
+ * @version 1.0
+ *
+ */
+public class DownloadTools {
+	private static Logger logger = Logger.getLogger("DownloadTools");
+	private static Core core = Core.getInstance();
+	private static MyHttpClient myHttpClient = core.getMyHttpClient();
+
+	/**
+	 * 处理下载任务
+	 *
+	 * @author https://github.com/yaphone
+	 * @date 2017年4月21日 下午11:00:25
+	 * @param url
+	 * @param msgId
+	 * @param path
+	 * @return
+	 */
+	public static Object getDownloadFn(BaseMsg msg, String type, String path) {
+		Map<String, String> headerMap = new HashMap<String, String>();
+		List<BasicNameValuePair> params = new ArrayList<BasicNameValuePair>();
+		String url = "";
+		if (type.equals(MsgTypeEnum.PIC.getType())) {
+			url = String.format(URLEnum.WEB_WX_GET_MSG_IMG.getUrl(), (String) core.getLoginInfo().get("url"));
+		} else if (type.equals(MsgTypeEnum.VOICE.getType())) {
+			url = String.format(URLEnum.WEB_WX_GET_VOICE.getUrl(), (String) core.getLoginInfo().get("url"));
+		} else if (type.equals(MsgTypeEnum.VIEDO.getType())) {
+			headerMap.put("Range", "bytes=0-");
+			url = String.format(URLEnum.WEB_WX_GET_VIEDO.getUrl(), (String) core.getLoginInfo().get("url"));
+		} else if (type.equals(MsgTypeEnum.MEDIA.getType())) {
+			headerMap.put("Range", "bytes=0-");
+			url = String.format(URLEnum.WEB_WX_GET_MEDIA.getUrl(), (String) core.getLoginInfo().get("fileUrl"));
+			params.add(new BasicNameValuePair("sender", msg.getFromUserName()));
+			params.add(new BasicNameValuePair("mediaid", msg.getMediaId()));
+			params.add(new BasicNameValuePair("filename", msg.getFileName()));
+		}
+		params.add(new BasicNameValuePair("msgid", msg.getNewMsgId()));
+		params.add(new BasicNameValuePair("skey", (String) core.getLoginInfo().get("skey")));
+		HttpEntity entity = myHttpClient.doGet(url, params, true, headerMap);
+		try {
+			OutputStream out = new FileOutputStream(path);
+			byte[] bytes = EntityUtils.toByteArray(entity);
+			out.write(bytes);
+			out.flush();
+			out.close();
+			// Tools.printQr(path);
+
+		} catch (Exception e) {
+			logger.info(e.getMessage());
+			return false;
+		}
+		return null;
+	};
+
+}

+ 2 - 0
ruoyi-modules/pom.xml

@@ -14,7 +14,9 @@
         <module>ruoyi-demo</module>
         <module>ruoyi-generator</module>
         <module>ruoyi-job</module>
+        <module>ruoyi-midjourney</module>
         <module>ruoyi-system</module>
+        <module>ruoyi-live</module>
     </modules>
 
     <artifactId>ruoyi-modules</artifactId>

+ 50 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/pom.xml

@@ -0,0 +1,50 @@
+<!--
+  ~ MIT License
+  ~
+  ~ Copyright (c) 2023 OrdinaryRoad
+  ~
+  ~ Permission is hereby granted, free of charge, to any person obtaining a copy
+  ~ of this software and associated documentation files (the "Software"), to deal
+  ~ in the Software without restriction, including without limitation the rights
+  ~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  ~ copies of the Software, and to permit persons to whom the Software is
+  ~ furnished to do so, subject to the following conditions:
+  ~
+  ~ The above copyright notice and this permission notice shall be included in all
+  ~ copies or substantial portions of the Software.
+  ~
+  ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  ~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  ~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  ~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  ~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  ~ SOFTWARE.
+  -->
+
+<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>
+
+    <parent>
+        <groupId>com.xmzs</groupId>
+        <artifactId>live-chat-client-commons</artifactId>
+        <version>1.0.0</version>
+    </parent>
+
+    <packaging>jar</packaging>
+
+    <artifactId>live-chat-client-commons-base</artifactId>
+    <name>ordinaryroad-live-chat-client-commons-base</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 32 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/constant/Constants.java

@@ -0,0 +1,32 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.constant;
+
+/**
+ * @author mjz
+ * @date 2023/8/26
+ */
+public class Constants {
+}

+ 51 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/exception/BaseException.java

@@ -0,0 +1,51 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.exception;
+
+/**
+ * @author mjz
+ * @date 2023/9/5
+ */
+public class BaseException extends RuntimeException {
+
+    public BaseException() {
+    }
+
+    public BaseException(String message) {
+        super(message);
+    }
+
+    public BaseException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public BaseException(Throwable cause) {
+        super(cause);
+    }
+
+    public BaseException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+}

+ 60 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/IBaseConnectionListener.java

@@ -0,0 +1,60 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.listener;
+
+
+/**
+ * 连接回调
+ *
+ * @author mjz
+ * @date 2023/8/26
+ */
+public interface IBaseConnectionListener<T> {
+
+    /**
+     * 连接建立成功
+     */
+    default void onConnected(T t) {
+        // ignore
+    }
+
+    /**
+     * 连接建立失败
+     *
+     * @param t
+     */
+    default void onConnectFailed(T t) {
+        // ignore
+    }
+
+    /**
+     * 连接断开
+     *
+     * @param t
+     */
+    default void onDisconnected(T t) {
+        // ignore
+    }
+}

+ 149 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/IBaseMsgListener.java

@@ -0,0 +1,149 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.listener;
+
+
+import tech.ordinaryroad.live.chat.client.commons.base.msg.BaseCmdMsg;
+import tech.ordinaryroad.live.chat.client.commons.base.msg.BaseMsg;
+import tech.ordinaryroad.live.chat.client.commons.base.msg.ICmdMsg;
+import tech.ordinaryroad.live.chat.client.commons.base.msg.IMsg;
+
+/**
+ * Base消息回调
+ *
+ * @author mjz
+ * @date 2023/8/26
+ */
+public interface IBaseMsgListener<T, CmdEnum extends Enum<CmdEnum>> {
+
+    /**
+     * 收到消息(所有消息)
+     *
+     * @param msg IMsg
+     */
+    default void onMsg(T t, IMsg msg) {
+        this.onMsg(msg);
+    }
+
+    default void onMsg(IMsg msg) {
+        // ignore
+    }
+
+    /**
+     * 收到cmd消息(所有cmd)
+     *
+     * @param cmd    CmdEnum
+     * @param cmdMsg BaseCmdMsg
+     */
+    default void onCmdMsg(T t, CmdEnum cmd, ICmdMsg<CmdEnum> cmdMsg) {
+        this.onCmdMsg(cmd, cmdMsg);
+    }
+
+    default void onCmdMsg(CmdEnum cmd, ICmdMsg<CmdEnum> cmdMsg) {
+        // ignore
+    }
+
+    /**
+     * 收到其他cmd消息(存在Enum,但Listener没有对应的回调)
+     *
+     * @param cmd    CmdEnum
+     * @param cmdMsg BaseCmdMsg
+     */
+    default void onOtherCmdMsg(T t, CmdEnum cmd, ICmdMsg<CmdEnum> cmdMsg) {
+        this.onOtherCmdMsg(cmd, cmdMsg);
+    }
+
+    default void onOtherCmdMsg(CmdEnum cmd, ICmdMsg<CmdEnum> cmdMsg) {
+        // ignore
+    }
+
+    /**
+     * 收到未知cmd消息
+     *
+     * @param cmdString 实际收到的cmd字符串
+     * @param msg       BaseMsg
+     */
+    default void onUnknownCmd(T t, String cmdString, IMsg msg) {
+        this.onUnknownCmd(cmdString, msg);
+    }
+
+    default void onUnknownCmd(String cmdString, IMsg msg) {
+        // ignore
+    }
+
+    /**
+     * 收到cmd消息(所有cmd)
+     *
+     * @param cmd    CmdEnum
+     * @param cmdMsg BaseCmdMsg
+     * @deprecated use {@link #onCmdMsg(T, Enum, ICmdMsg)}
+     */
+    default void onCmdMsg(T t, CmdEnum cmd, BaseCmdMsg<CmdEnum> cmdMsg) {
+        this.onCmdMsg(cmd, cmdMsg);
+    }
+
+    /**
+     * @deprecated use {@link #onCmdMsg(Enum, ICmdMsg)}
+     */
+    default void onCmdMsg(CmdEnum cmd, BaseCmdMsg<CmdEnum> cmdMsg) {
+        // ignore
+    }
+
+    /**
+     * 收到其他cmd消息(存在Enum,但Listener没有对应的回调)
+     *
+     * @param cmd    CmdEnum
+     * @param cmdMsg BaseCmdMsg
+     * @deprecated use {@link #onOtherCmdMsg(T, Enum, ICmdMsg)}
+     */
+    default void onOtherCmdMsg(T t, CmdEnum cmd, BaseCmdMsg<CmdEnum> cmdMsg) {
+        this.onOtherCmdMsg(cmd, cmdMsg);
+    }
+
+    /**
+     * @deprecated use {@link #onOtherCmdMsg(Enum, ICmdMsg)}
+     */
+    default void onOtherCmdMsg(CmdEnum cmd, BaseCmdMsg<CmdEnum> cmdMsg) {
+        // ignore
+    }
+
+    /**
+     * 收到未知cmd消息
+     *
+     * @param cmdString 实际收到的cmd字符串
+     * @param msg       BaseMsg
+     * @deprecated use {@link #onUnknownCmd(T, String, IMsg)}
+     */
+    default void onUnknownCmd(T t, String cmdString, BaseMsg msg) {
+        this.onUnknownCmd(cmdString, msg);
+    }
+
+    /**
+     * @deprecated use {@link #onUnknownCmd(String, IMsg)}
+     */
+    default void onUnknownCmd(String cmdString, BaseMsg msg) {
+        // ignore
+    }
+}

+ 46 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/IDanmuMsgListener.java

@@ -0,0 +1,46 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.listener;
+
+
+/**
+ * 弹幕消息回调
+ *
+ * @author mjz
+ * @since 0.0.6
+ */
+public interface IDanmuMsgListener<T, DanmuMsg> {
+
+    /**
+     * 收到弹幕
+     */
+    default void onDanmuMsg(T t, DanmuMsg msg) {
+        this.onDanmuMsg(msg);
+    }
+
+    default void onDanmuMsg(DanmuMsg msg) {
+        // ignore
+    }
+}

+ 47 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/IEnterRoomMsgListener.java

@@ -0,0 +1,47 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.listener;
+
+
+/**
+ * 进入房间消息回调
+ *
+ * @author mjz
+ * @date 2023/12/14
+ * @since 0.0.16
+ */
+public interface IEnterRoomMsgListener<T, EnterRoomMsg> {
+
+    /**
+     * 用户进入房间
+     */
+    default void onEnterRoomMsg(T t, EnterRoomMsg msg) {
+        this.onEnterRoomMsg(msg);
+    }
+
+    default void onEnterRoomMsg(EnterRoomMsg msg) {
+        // ignore
+    }
+}

+ 47 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/IGiftMsgListener.java

@@ -0,0 +1,47 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.listener;
+
+
+/**
+ * 礼物消息回调
+ *
+ * @author mjz
+ * @since 0.0.8
+ */
+public interface IGiftMsgListener<T, GiftMsg> {
+
+
+    /**
+     * 收到礼物
+     */
+    default void onGiftMsg(T t, GiftMsg msg) {
+        this.onGiftMsg(msg);
+    }
+
+    default void onGiftMsg(GiftMsg msg) {
+        // ignore
+    }
+}

+ 46 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/ILikeMsgListener.java

@@ -0,0 +1,46 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.listener;
+
+
+/**
+ * 点赞消息回调
+ *
+ * @author mjz
+ * @since 0.2.0
+ */
+public interface ILikeMsgListener<T, LikeMsg> {
+
+    /**
+     * 收到点赞
+     */
+    default void onLikeMsg(T t, LikeMsg msg) {
+        this.onLikeMsg(msg);
+    }
+
+    default void onLikeMsg(LikeMsg msg) {
+        // ignore
+    }
+}

+ 47 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/listener/ISuperChatMsgListener.java

@@ -0,0 +1,47 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.listener;
+
+
+/**
+ * 醒目留言消息回调
+ *
+ * @author mjz
+ * @date 2023/9/24
+ * @since 0.0.11
+ */
+public interface ISuperChatMsgListener<T, SuperChatMsg> {
+
+    /**
+     * 收到醒目留言
+     */
+    default void onSuperChatMsg(T t, SuperChatMsg msg) {
+        this.onSuperChatMsg(msg);
+    }
+
+    default void onSuperChatMsg(SuperChatMsg msg) {
+        // ignore
+    }
+}

+ 33 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/BaseCmdMsg.java

@@ -0,0 +1,33 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.msg;
+
+/**
+ * @author mjz
+ * @date 2023/8/26
+ */
+public abstract class BaseCmdMsg<CmdEnum extends Enum<CmdEnum>> extends BaseMsg
+        implements ICmdMsg<CmdEnum> {
+}

+ 74 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/BaseMsg.java

@@ -0,0 +1,74 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.msg;
+
+import com.fasterxml.jackson.annotation.JsonAnyGetter;
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import tech.ordinaryroad.live.chat.client.commons.base.exception.BaseException;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author mjz
+ * @date 2023/8/26
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public abstract class BaseMsg implements IMsg {
+
+    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+            .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
+
+    /**
+     * 未知属性都放在这
+     */
+    private final Map<String, JsonNode> unknownProperties = new HashMap<>();
+
+    @JsonAnyGetter
+    public Map<String, JsonNode> getUnknownProperties() {
+        return unknownProperties;
+    }
+
+    @JsonAnySetter
+    public void setOther(String key, JsonNode value) {
+        this.unknownProperties.put(key, value);
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return OBJECT_MAPPER.writeValueAsString(this);
+        } catch (JsonProcessingException e) {
+            throw new BaseException(e);
+        }
+    }
+}

+ 38 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/ICmdMsg.java

@@ -0,0 +1,38 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.msg;
+
+/**
+ * @author mjz
+ * @date 2023/10/2
+ */
+public interface ICmdMsg<CmdEnum extends Enum<CmdEnum>> extends IMsg {
+
+    String getCmd();
+
+    void setCmd(String cmd);
+
+    CmdEnum getCmdEnum();
+}

+ 67 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/IDanmuMsg.java

@@ -0,0 +1,67 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.msg;
+
+/**
+ * @author mjz
+ * @date 2023/9/8
+ */
+public interface IDanmuMsg extends IMsg {
+
+    /**
+     * 粉丝牌名称
+     */
+    String getBadgeName();
+
+    /**
+     * 粉丝牌等级
+     */
+    byte getBadgeLevel();
+
+    /**
+     * 弹幕发送者id
+     */
+    String getUid();
+
+    /**
+     * 弹幕发送者用户名
+     */
+    String getUsername();
+
+    /**
+     * 弹幕发送者头像地址
+     *
+     * @since 0.0.11
+     */
+    default String getUserAvatar() {
+        return null;
+    }
+
+    /**
+     * 弹幕内容
+     */
+    String getContent();
+
+}

+ 62 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/IEnterRoomMsg.java

@@ -0,0 +1,62 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.msg;
+
+/**
+ * 入房消息
+ *
+ * @author mjz
+ * @date 2023/12/26
+ * @since 0.0.16
+ */
+public interface IEnterRoomMsg extends IMsg {
+
+    /**
+     * 粉丝牌名称
+     */
+    String getBadgeName();
+
+    /**
+     * 粉丝牌等级
+     */
+    byte getBadgeLevel();
+
+    /**
+     * 用户id
+     */
+    String getUid();
+
+    /**
+     * 用户名
+     */
+    String getUsername();
+
+    /**
+     * 头像地址
+     */
+    default String getUserAvatar() {
+        return null;
+    }
+}

+ 100 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/IGiftMsg.java

@@ -0,0 +1,100 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.msg;
+
+/**
+ * @author mjz
+ * @date 2023/9/8
+ */
+public interface IGiftMsg extends IMsg {
+
+    /**
+     * 粉丝牌名称
+     */
+    default String getBadgeName() {
+        return "";
+    }
+
+    /**
+     * 粉丝牌等级
+     */
+    default byte getBadgeLevel() {
+        return 0;
+    }
+
+    /**
+     * 发送方id
+     */
+    String getUid();
+
+    /**
+     * 发送方用户名
+     */
+    String getUsername();
+
+    /**
+     * 发送方头像地址
+     *
+     * @since 0.0.11
+     */
+    default String getUserAvatar() {
+        return null;
+    }
+
+    /**
+     * 礼物名称
+     */
+    String getGiftName();
+
+    /**
+     * 礼物图像地址
+     */
+    String getGiftImg();
+
+    /**
+     * 礼物id
+     */
+    String getGiftId();
+
+    /**
+     * 礼物数量
+     */
+    int getGiftCount();
+
+    /**
+     * 单个礼物价格
+     */
+    int getGiftPrice();
+
+    /**
+     * 接收方id
+     */
+    String getReceiveUid();
+
+    /**
+     * 接收方用户名
+     */
+    String getReceiveUsername();
+}

+ 71 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/ILikeMsg.java

@@ -0,0 +1,71 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.msg;
+
+/**
+ * @author mjz
+ * @date 2024/1/31
+ * @since 0.2.0
+ */
+public interface ILikeMsg extends IMsg {
+
+    /**
+     * 粉丝牌名称
+     */
+    default String getBadgeName(){
+        return "";
+    }
+
+    /**
+     * 粉丝牌等级
+     */
+    default byte getBadgeLevel(){
+        return 0;
+    }
+
+    /**
+     * 点赞者id
+     */
+    String getUid();
+
+    /**
+     * 点赞者用户名
+     */
+    String getUsername();
+
+    /**
+     * 点赞者头像地址
+     */
+    default String getUserAvatar() {
+        return null;
+    }
+
+    /**
+     * 点赞数
+     */
+    default int getClickCount() {
+        return 1;
+    }
+}

+ 34 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/IMsg.java

@@ -0,0 +1,34 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.msg;
+
+import java.io.Serializable;
+
+/**
+ * @author mjz
+ * @date 2023/8/26
+ */
+public interface IMsg extends Serializable {
+}

+ 49 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-base/src/main/java/tech/ordinaryroad/live/chat/client/commons/base/msg/ISuperChatMsg.java

@@ -0,0 +1,49 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.base.msg;
+
+/**
+ * 醒目留言
+ *
+ * @author mjz
+ * @date 2023/9/22
+ */
+public interface ISuperChatMsg extends IDanmuMsg {
+
+    /**
+     * 醒目留言持续时间,单位秒
+     */
+    int getDuration();
+
+    @Override
+    default String getBadgeName() {
+        return "";
+    }
+
+    @Override
+    default byte getBadgeLevel() {
+        return 0;
+    }
+}

+ 54 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-client/pom.xml

@@ -0,0 +1,54 @@
+<!--
+  ~ MIT License
+  ~
+  ~ Copyright (c) 2023 OrdinaryRoad
+  ~
+  ~ Permission is hereby granted, free of charge, to any person obtaining a copy
+  ~ of this software and associated documentation files (the "Software"), to deal
+  ~ in the Software without restriction, including without limitation the rights
+  ~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  ~ copies of the Software, and to permit persons to whom the Software is
+  ~ furnished to do so, subject to the following conditions:
+  ~
+  ~ The above copyright notice and this permission notice shall be included in all
+  ~ copies or substantial portions of the Software.
+  ~
+  ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  ~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  ~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  ~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  ~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  ~ SOFTWARE.
+  -->
+
+<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>
+
+    <parent>
+        <groupId>com.xmzs</groupId>
+        <artifactId>live-chat-client-commons</artifactId>
+        <version>1.0.0</version>
+    </parent>
+    <packaging>jar</packaging>
+
+    <artifactId>live-chat-client-commons-client</artifactId>
+    <name>live-chat-client-commons-client</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.xmzs</groupId>
+            <artifactId>live-chat-client-commons-base</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.xmzs</groupId>
+            <artifactId>live-chat-client-commons-util</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 200 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-client/src/main/java/tech/ordinaryroad/live/chat/client/commons/client/BaseLiveChatClient.java

@@ -0,0 +1,200 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.client;
+
+import lombok.Getter;
+import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseMsgListener;
+import tech.ordinaryroad.live.chat.client.commons.client.config.BaseLiveChatClientConfig;
+import tech.ordinaryroad.live.chat.client.commons.client.enums.ClientStatusEnums;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * @author mjz
+ * @date 2023/8/26
+ */
+public abstract class BaseLiveChatClient<
+        Config extends BaseLiveChatClientConfig,
+        MsgListener extends IBaseMsgListener<?, ?>
+        > implements IBaseLiveChatClient<MsgListener> {
+
+    private final Config config;
+    @Getter
+    private volatile ClientStatusEnums status = ClientStatusEnums.NEW;
+    protected PropertyChangeSupport statusChangeSupport = new PropertyChangeSupport(status);
+    protected volatile boolean cancelReconnect = false;
+    protected final List<MsgListener> msgListeners = Collections.synchronizedList(new ArrayList<>());
+
+    protected BaseLiveChatClient(Config config) {
+        this.config = config;
+    }
+
+    public Config getConfig() {
+        return config;
+    }
+
+    @Override
+    public void connect(Runnable success) {
+        this.connect(success, null);
+    }
+
+    @Override
+    public void connect() {
+        this.connect(null, null);
+    }
+
+    @Override
+    public void disconnect(boolean cancelReconnect) {
+        this.cancelReconnect = cancelReconnect;
+        this.disconnect();
+    }
+
+    @Override
+    public void send(Object msg) {
+        this.send(msg, null, null);
+    }
+
+    @Override
+    public void send(Object msg, Runnable success) {
+        this.send(msg, success, null);
+    }
+
+    @Override
+    public void send(Object msg, Consumer<Throwable> failed) {
+        this.send(msg, null, failed);
+    }
+
+    @Override
+    public void sendDanmu(Object danmu) {
+        this.sendDanmu(danmu, null, null);
+    }
+
+    @Override
+    public void sendDanmu(Object danmu, Runnable success) {
+        this.sendDanmu(danmu, success, null);
+    }
+
+    @Override
+    public void sendDanmu(Object danmu, Consumer<Throwable> failed) {
+        this.sendDanmu(danmu, null, failed);
+    }
+
+    @Override
+    public void clickLike(int count) {
+        this.clickLike(count, null, null);
+    }
+
+    @Override
+    public void clickLike(int count, Runnable success) {
+        this.clickLike(count, success, null);
+    }
+
+    @Override
+    public void clickLike(int count, Consumer<Throwable> failed) {
+        this.clickLike(count, null, failed);
+    }
+
+    protected abstract void tryReconnect();
+
+    protected abstract String getWebSocketUriString();
+
+    /**
+     * 判断是否处于某个状态,或者处于后续状态
+     *
+     * @param status {@link ClientStatusEnums}
+     * @return false: 还没有到达该状态
+     */
+    protected boolean checkStatus(ClientStatusEnums status) {
+        return this.status.getCode() >= Objects.requireNonNull(status).getCode();
+    }
+
+    protected void setStatus(ClientStatusEnums status) {
+        ClientStatusEnums oldStatus = this.status;
+        if (oldStatus != status) {
+            this.status = status;
+            this.statusChangeSupport.firePropertyChange("status", oldStatus, status);
+        }
+    }
+
+    public void addStatusChangeListener(PropertyChangeListener listener) {
+        this.statusChangeSupport.addPropertyChangeListener(listener);
+    }
+
+    public void removeStatusChangeListener(PropertyChangeListener listener) {
+        this.statusChangeSupport.removePropertyChangeListener(listener);
+    }
+
+    @Override
+    public void destroy() {
+        for (PropertyChangeListener propertyChangeListener : this.statusChangeSupport.getPropertyChangeListeners()) {
+            this.statusChangeSupport.removePropertyChangeListener(propertyChangeListener);
+        }
+        this.msgListeners.clear();
+    }
+
+    @Override
+    public boolean addMsgListener(MsgListener msgListener) {
+        if (msgListener == null) {
+            return false;
+        }
+        return this.msgListeners.add(msgListener);
+    }
+
+    @Override
+    public boolean addMsgListeners(List<MsgListener> msgListeners) {
+        if (msgListeners == null || msgListeners.isEmpty()) {
+            return false;
+        }
+        return this.msgListeners.addAll(msgListeners);
+    }
+
+    @Override
+    public boolean removeMsgListener(MsgListener msgListener) {
+        if (msgListener == null) {
+            return false;
+        }
+        return this.msgListeners.remove(msgListener);
+    }
+
+    @Override
+    public boolean removeMsgListeners(List<MsgListener> msgListeners) {
+        if (msgListeners == null || msgListeners.isEmpty()) {
+            return false;
+        }
+        return this.msgListeners.removeAll(msgListeners);
+    }
+
+    @Override
+    public void removeAllMsgListeners() {
+        this.msgListeners.clear();
+    }
+
+}

+ 135 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-client/src/main/java/tech/ordinaryroad/live/chat/client/commons/client/IBaseLiveChatClient.java

@@ -0,0 +1,135 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.client;
+
+import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseMsgListener;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * @author mjz
+ * @date 2023/9/5
+ */
+public interface IBaseLiveChatClient<MsgListener extends IBaseMsgListener<?, ?>> {
+
+    void init();
+
+    boolean addMsgListener(MsgListener msgListener);
+
+    boolean addMsgListeners(List<MsgListener> msgListeners);
+
+    boolean removeMsgListener(MsgListener msgListener);
+
+    boolean removeMsgListeners(List<MsgListener> msgListeners);
+
+    void removeAllMsgListeners();
+
+    void connect(Runnable success, Consumer<Throwable> failed);
+
+    void connect(Runnable success);
+
+    void connect();
+
+    /**
+     * 手动断开连接
+     *
+     * @param cancelReconnect 取消本次的自动重连(如果启用自动重连)
+     */
+    void disconnect(boolean cancelReconnect);
+
+    void disconnect();
+
+    void destroy();
+
+    void send(Object msg);
+
+    void send(Object msg, Runnable success, Consumer<Throwable> failed);
+
+    void send(Object msg, Runnable success);
+
+    void send(Object msg, Consumer<Throwable> failed);
+
+    /**
+     * 发送弹幕
+     *
+     * @param danmu 弹幕内容
+     * @since 0.0.6
+     */
+    void sendDanmu(Object danmu);
+
+    /**
+     * 发送弹幕
+     *
+     * @param danmu 弹幕内容
+     * @since 0.0.6
+     */
+    void sendDanmu(Object danmu, Runnable success, Consumer<Throwable> failed);
+
+    /**
+     * 发送弹幕
+     *
+     * @param danmu 弹幕内容
+     * @since 0.0.6
+     */
+    void sendDanmu(Object danmu, Runnable success);
+
+    /**
+     * 发送弹幕
+     *
+     * @param danmu 弹幕内容
+     * @since 0.0.6
+     */
+    void sendDanmu(Object danmu, Consumer<Throwable> failed);
+
+    /**
+     * 为直播间点赞
+     *
+     * @since 0.2.0
+     */
+    void clickLike(int count);
+
+    /**
+     * 为直播间点赞
+     *
+     * @since 0.2.0
+     */
+    void clickLike(int count, Runnable success, Consumer<Throwable> failed);
+
+    /**
+     * 为直播间点赞
+     *
+     * @since 0.2.0
+     */
+    void clickLike(int count, Runnable success);
+
+    /**
+     * 为直播间点赞
+     *
+     * @since 0.2.0
+     */
+    void clickLike(int count, Consumer<Throwable> failed);
+
+}

+ 139 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-client/src/main/java/tech/ordinaryroad/live/chat/client/commons/client/config/BaseLiveChatClientConfig.java

@@ -0,0 +1,139 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.client.config;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import tech.ordinaryroad.live.chat.client.commons.base.exception.BaseException;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+
+/**
+ * 直播间弹幕客户端配置
+ *
+ * @author mjz
+ * @date 2023/8/26
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@SuperBuilder(toBuilder = true)
+public abstract class BaseLiveChatClientConfig {
+
+    protected PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);
+    public static final long DEFAULT_HEARTBEAT_INITIAL_DELAY = 15;
+    public static final long DEFAULT_HEARTBEAT_PERIOD = 25;
+    public static final long DEFAULT_MIN_SEND_DANMU_PERIOD = 3000L;
+
+    private String websocketUri;
+
+    /**
+     * 浏览器中的Cookie
+     */
+    private String cookie;
+
+    /**
+     * 直播间id
+     */
+    private Object roomId;
+
+    /**
+     * 是否启用自动重连
+     */
+    @Builder.Default
+    private boolean autoReconnect = Boolean.TRUE;
+
+    /**
+     * 重试延迟时间(秒),默认5s后重试
+     */
+    @Builder.Default
+    private int reconnectDelay = 5;
+
+    /**
+     * 首次发送心跳包的延迟时间(秒)
+     */
+    @Builder.Default
+    private long heartbeatInitialDelay = DEFAULT_HEARTBEAT_INITIAL_DELAY;
+
+    /**
+     * 心跳包发送周期(秒)
+     */
+    @Builder.Default
+    private long heartbeatPeriod = DEFAULT_HEARTBEAT_PERIOD;
+
+    /**
+     * 最小发送弹幕时间间隔(毫秒)
+     */
+    @Builder.Default
+    private long minSendDanmuPeriod = DEFAULT_MIN_SEND_DANMU_PERIOD;
+
+    public void setCookie(String cookie) {
+        String oldValue = this.cookie;
+        this.cookie = cookie;
+        this.propertyChangeSupport.firePropertyChange("cookie", oldValue, cookie);
+    }
+
+    public void setRoomId(Object roomId) {
+        if (!(roomId instanceof Number || roomId instanceof String)) {
+            throw new BaseException("房间ID仅支持数字或字符串,所传参数类型:" + roomId.getClass() + "值:" + roomId);
+        }
+        Object oldValue = this.roomId;
+        this.roomId = roomId;
+        this.propertyChangeSupport.firePropertyChange("roomId", oldValue, roomId);
+    }
+
+    public void setWebsocketUri(String websocketUri) {
+        String oldValue = this.websocketUri;
+        this.websocketUri = websocketUri;
+        this.propertyChangeSupport.firePropertyChange("websocketUri", oldValue, websocketUri);
+    }
+
+    public void setMinSendDanmuPeriod(long minSendDanmuPeriod) {
+        long oldValue = this.minSendDanmuPeriod;
+        this.minSendDanmuPeriod = minSendDanmuPeriod;
+        this.propertyChangeSupport.firePropertyChange("minSendDanmuPeriod", oldValue, minSendDanmuPeriod);
+    }
+
+    public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+        this.propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
+    }
+
+    public void addPropertyChangeListener(PropertyChangeListener listener) {
+        this.propertyChangeSupport.addPropertyChangeListener(listener);
+    }
+
+    public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
+        this.propertyChangeSupport.removePropertyChangeListener(propertyName, listener);
+    }
+
+    public void removePropertyChangeListener(PropertyChangeListener listener) {
+        this.propertyChangeSupport.removePropertyChangeListener(listener);
+    }
+}

+ 83 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-client/src/main/java/tech/ordinaryroad/live/chat/client/commons/client/enums/ClientStatusEnums.java

@@ -0,0 +1,83 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.client.enums;
+
+/**
+ * @author mjz
+ * @date 2023/8/26
+ */
+public enum ClientStatusEnums {
+    /**
+     * 新创建
+     */
+    NEW(0),
+
+    /**
+     * 已初始化
+     */
+    INITIALIZED(1),
+
+    /**
+     * 连接中
+     */
+    CONNECTING(100),
+
+    /**
+     * 重新连接中
+     */
+    RECONNECTING(101),
+
+    /**
+     * 已连接
+     */
+    CONNECTED(200),
+
+    /**
+     * 连接失败
+     */
+    CONNECT_FAILED(401),
+
+    /**
+     * 已断开连接
+     */
+    DISCONNECTED(400),
+
+    /**
+     * 已销毁
+     */
+    DESTROYED(-1),
+    ;
+
+    public int getCode() {
+        return code;
+    }
+
+    ClientStatusEnums(int order) {
+        this.code = order;
+    }
+
+    private final int code;
+
+}

+ 24 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-util/pom.xml

@@ -0,0 +1,24 @@
+<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>
+    <parent>
+        <groupId>com.xmzs</groupId>
+        <artifactId>live-chat-client-commons</artifactId>
+        <version>1.0.0</version>
+    </parent>
+    <packaging>jar</packaging>
+
+    <artifactId>live-chat-client-commons-util</artifactId>
+    <name>live-chat-client-commons-util</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 80 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-util/src/main/java/tech/ordinaryroad/live/chat/client/commons/util/OrLiveChatCookieUtil.java

@@ -0,0 +1,80 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.util;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+
+import java.net.HttpCookie;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+/**
+ * @author mjz
+ * @date 2023/8/27
+ */
+public class OrLiveChatCookieUtil {
+
+    public static String toString(List<HttpCookie> cookies) {
+        if (CollUtil.isEmpty(cookies)) {
+            return StrUtil.EMPTY;
+        }
+
+        return cookies.stream().map(httpCookie -> {
+            httpCookie.setVersion(0);
+            return httpCookie.toString();
+        }).collect(Collectors.joining("; "));
+    }
+
+    public static Map<String, String> parseCookieString(String cookies) {
+        Map<String, String> map = new HashMap<>();
+        if (StrUtil.isNotBlank(cookies) && !StrUtil.isNullOrUndefined(cookies)) {
+            try {
+                String[] split = cookies.split("; ");
+                for (String s : split) {
+                    String[] split1 = s.split("=");
+                    map.put(split1[0], split1[1]);
+                }
+            } catch (Exception e) {
+                throw new RuntimeException("cookie解析失败 " + cookies, e);
+            }
+        }
+        return map;
+    }
+
+    public static String getCookieByName(Map<String, String> cookieMap, String name, Supplier<String> supplier) {
+        String str = MapUtil.getStr(cookieMap, name);
+        return str == null ? supplier.get() : str;
+    }
+
+    public static String getCookieByName(String cookie, String name, Supplier<String> supplier) {
+        String str = MapUtil.getStr(parseCookieString(cookie), name);
+        return str == null ? supplier.get() : str;
+    }
+}

+ 40 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-util/src/main/java/tech/ordinaryroad/live/chat/client/commons/util/OrLiveChatNumberUtil.java

@@ -0,0 +1,40 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.util;
+
+import cn.hutool.core.util.NumberUtil;
+import cn.hutool.core.util.StrUtil;
+
+/**
+ * @author mjz
+ * @date 2023/12/2
+ */
+public class OrLiveChatNumberUtil extends NumberUtil {
+
+    public static long parseLong(Object object){
+        return NumberUtil.parseLong(StrUtil.toStringOrNull(object));
+    }
+
+}

+ 50 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-util/src/main/java/tech/ordinaryroad/live/chat/client/commons/util/OrLiveChatReflectUtil.java

@@ -0,0 +1,50 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.util;
+
+import cn.hutool.core.util.ReflectUtil;
+
+import java.lang.reflect.Method;
+
+/**
+ * @author mjz
+ * @date 2023/8/28
+ */
+public class OrLiveChatReflectUtil extends ReflectUtil {
+
+    public static Method getGetterMethod(Class<?> objectClass, String key) {
+        Method method;
+        if (key.startsWith("is")) {
+            method = ReflectUtil.getMethodByNameIgnoreCase(objectClass, key);
+            if (method == null) {
+                ReflectUtil.getMethodByNameIgnoreCase(objectClass, "get" + key);
+            }
+        } else {
+            method = ReflectUtil.getMethodByNameIgnoreCase(objectClass, "get" + key);
+        }
+        return method;
+    }
+
+}

+ 54 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/live-chat-client-commons-util/src/main/java/tech/ordinaryroad/live/chat/client/commons/util/OrLocalDateTimeUtil.java

@@ -0,0 +1,54 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.commons.util;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+/**
+ * @author mjz
+ * @date 2023/9/7
+ */
+public class OrLocalDateTimeUtil extends LocalDateTimeUtil {
+
+    public static ZoneId ZONE_ID_CTT = ZoneId.of(ZoneId.SHORT_IDS.get("CTT"));
+
+    /**
+     * 获取中国标准时间的当前时间戳(毫秒)
+     */
+    public static long zonedCurrentTimeMillis() {
+        ZonedDateTime now = ZonedDateTime.now(ZONE_ID_CTT);
+        return now.toEpochSecond() * 1000 + now.getNano() / 1_000_000;
+    }
+
+    /**
+     * 获取中国标准时间的当前时间戳(秒)
+     */
+    public static long zonedCurrentTimeSecs() {
+        return ZonedDateTime.now(ZONE_ID_CTT).toEpochSecond();
+    }
+}

+ 43 - 0
ruoyi-modules/ruoyi-live/live-chat-client-commons/pom.xml

@@ -0,0 +1,43 @@
+<!--
+  ~ MIT License
+  ~
+  ~ Copyright (c) 2023 OrdinaryRoad
+  ~
+  ~ Permission is hereby granted, free of charge, to any person obtaining a copy
+  ~ of this software and associated documentation files (the "Software"), to deal
+  ~ in the Software without restriction, including without limitation the rights
+  ~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  ~ copies of the Software, and to permit persons to whom the Software is
+  ~ furnished to do so, subject to the following conditions:
+  ~
+  ~ The above copyright notice and this permission notice shall be included in all
+  ~ copies or substantial portions of the Software.
+  ~
+  ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  ~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  ~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  ~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  ~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  ~ SOFTWARE.
+  -->
+
+<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/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.xmzs</groupId>
+        <artifactId>ruoyi-live</artifactId>
+        <version>1.0.0</version>
+    </parent>
+
+    <packaging>pom</packaging>
+
+    <artifactId>live-chat-client-commons</artifactId>
+
+    <modules>
+        <module>live-chat-client-commons-base</module>
+        <module>live-chat-client-commons-util</module>
+        <module>live-chat-client-commons-client</module>
+    </modules>
+</project>

+ 54 - 0
ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty-client/pom.xml

@@ -0,0 +1,54 @@
+<!--
+  ~ MIT License
+  ~
+  ~ Copyright (c) 2023 OrdinaryRoad
+  ~
+  ~ Permission is hereby granted, free of charge, to any person obtaining a copy
+  ~ of this software and associated documentation files (the "Software"), to deal
+  ~ in the Software without restriction, including without limitation the rights
+  ~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  ~ copies of the Software, and to permit persons to whom the Software is
+  ~ furnished to do so, subject to the following conditions:
+  ~
+  ~ The above copyright notice and this permission notice shall be included in all
+  ~ copies or substantial portions of the Software.
+  ~
+  ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  ~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  ~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  ~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  ~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  ~ SOFTWARE.
+  -->
+
+<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>
+    <parent>
+        <groupId>com.xmzs</groupId>
+        <artifactId>live-chat-client-servers</artifactId>
+        <version>1.0.0</version>
+    </parent>
+
+    <packaging>jar</packaging>
+
+    <artifactId>live-chat-client-servers-netty-client</artifactId>
+    <name>live-chat-client-servers-netty</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.xmzs</groupId>
+            <artifactId>live-chat-client-commons-client</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.xmzs</groupId>
+            <artifactId>live-chat-client-servers-netty</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 349 - 0
ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty-client/src/main/java/tech/ordinaryroad/live/chat/client/servers/netty/client/base/BaseNettyClient.java

@@ -0,0 +1,349 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.servers.netty.client.base;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.*;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http.HttpClientCodec;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.stream.ChunkedWriteHandler;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import tech.ordinaryroad.live.chat.client.commons.base.exception.BaseException;
+import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseConnectionListener;
+import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseMsgListener;
+import tech.ordinaryroad.live.chat.client.commons.base.msg.IMsg;
+import tech.ordinaryroad.live.chat.client.commons.client.BaseLiveChatClient;
+import tech.ordinaryroad.live.chat.client.commons.client.enums.ClientStatusEnums;
+import tech.ordinaryroad.live.chat.client.servers.netty.client.config.BaseNettyClientConfig;
+import tech.ordinaryroad.live.chat.client.servers.netty.handler.base.BaseBinaryFrameHandler;
+import tech.ordinaryroad.live.chat.client.servers.netty.handler.base.BaseConnectionHandler;
+
+import javax.net.ssl.SSLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+/**
+ * @author mjz
+ * @date 2023/8/26
+ */
+@Slf4j
+public abstract class BaseNettyClient
+        <Config extends BaseNettyClientConfig,
+                CmdEnum extends Enum<CmdEnum>,
+                Msg extends IMsg,
+                MsgListener extends IBaseMsgListener<BinaryFrameHandler, CmdEnum>,
+                ConnectionHandler extends BaseConnectionHandler<ConnectionHandler>,
+                BinaryFrameHandler extends BaseBinaryFrameHandler<BinaryFrameHandler, CmdEnum, Msg, MsgListener>
+                >
+        extends BaseLiveChatClient<Config, MsgListener> {
+
+    @Getter
+    private final EventLoopGroup workerGroup;
+    @Getter
+    private final Bootstrap bootstrap = new Bootstrap();
+    private BinaryFrameHandler binaryFrameHandler;
+    private ConnectionHandler connectionHandler;
+    private IBaseConnectionListener<ConnectionHandler> connectionListener;
+    private Channel channel;
+    @Getter
+    private URI websocketUri;
+    protected IBaseConnectionListener<ConnectionHandler> clientConnectionListener;
+    /**
+     * 控制弹幕发送频率
+     */
+    private volatile long lastSendDanmuTimeInMillis;
+
+    public abstract ConnectionHandler initConnectionHandler(IBaseConnectionListener<ConnectionHandler> clientConnectionListener);
+
+    public abstract BinaryFrameHandler initBinaryFrameHandler();
+
+    protected BaseNettyClient(Config config, EventLoopGroup workerGroup, IBaseConnectionListener<ConnectionHandler> connectionListener) {
+        super(config);
+        this.workerGroup = workerGroup;
+        this.connectionListener = connectionListener;
+    }
+
+    public void onConnected(ConnectionHandler connectionHandler) {
+        this.setStatus(ClientStatusEnums.CONNECTED);
+        if (this.connectionListener != null) {
+            this.connectionListener.onConnected(connectionHandler);
+        }
+    }
+
+    public void onConnectFailed(ConnectionHandler connectionHandler) {
+        this.setStatus(ClientStatusEnums.CONNECT_FAILED);
+        tryReconnect();
+        if (this.connectionListener != null) {
+            this.connectionListener.onConnectFailed(connectionHandler);
+        }
+    }
+
+    public void onDisconnected(ConnectionHandler connectionHandler) {
+        this.setStatus(ClientStatusEnums.DISCONNECTED);
+        tryReconnect();
+        if (this.connectionListener != null) {
+            this.connectionListener.onDisconnected(connectionHandler);
+        }
+    }
+
+    @Override
+    public void init() {
+        if (checkStatus(ClientStatusEnums.INITIALIZED)) {
+            return;
+        }
+        try {
+            this.websocketUri = new URI(getWebSocketUriString());
+            SslContext sslCtx = SslContextBuilder.forClient().build();
+
+            this.clientConnectionListener = new IBaseConnectionListener<ConnectionHandler>() {
+                @Override
+                public void onConnected(ConnectionHandler connectionHandler) {
+                    BaseNettyClient.this.onConnected(connectionHandler);
+                }
+
+                @Override
+                public void onConnectFailed(ConnectionHandler connectionHandler) {
+                    BaseNettyClient.this.onConnectFailed(connectionHandler);
+                }
+
+                @Override
+                public void onDisconnected(ConnectionHandler connectionHandler) {
+                    BaseNettyClient.this.onDisconnected(connectionHandler);
+                }
+            };
+            this.binaryFrameHandler = this.initBinaryFrameHandler();
+            this.connectionHandler = this.initConnectionHandler(this.clientConnectionListener);
+
+            this.bootstrap.group(this.workerGroup)
+                    // 创建Channel
+                    .channel(NioSocketChannel.class)
+                    .remoteAddress(this.websocketUri.getHost(), getInetPort())
+                    .option(ChannelOption.TCP_NODELAY, true)
+                    .option(ChannelOption.SO_KEEPALIVE, true)
+                    // Channel配置
+                    .handler(new ChannelInitializer<SocketChannel>() {
+                        @Override
+                        protected void initChannel(SocketChannel ch) {
+                            // 责任链
+                            ChannelPipeline pipeline = ch.pipeline();
+
+                            // 放到第一位 addFirst 支持wss链接服务端
+                            pipeline.addFirst(sslCtx.newHandler(ch.alloc(), BaseNettyClient.this.websocketUri.getHost(), getInetPort()));
+
+                            // 添加一个http的编解码器
+                            pipeline.addLast(new HttpClientCodec());
+                            // 添加一个用于支持大数据流的支持
+                            pipeline.addLast(new ChunkedWriteHandler());
+                            // 添加一个聚合器,这个聚合器主要是将HttpMessage聚合成FullHttpRequest/Response
+                            pipeline.addLast(new HttpObjectAggregator(BaseNettyClient.this.getConfig().getAggregatorMaxContentLength()));
+
+                            // 连接处理器
+                            pipeline.addLast(BaseNettyClient.this.connectionHandler);
+                            // 弹幕处理器
+                            pipeline.addLast(BaseNettyClient.this.binaryFrameHandler);
+                        }
+                    });
+            this.setStatus(ClientStatusEnums.INITIALIZED);
+        } catch (URISyntaxException e) {
+            throw new BaseException(e);
+        } catch (SSLException e) {
+            throw new BaseException(e);
+        }
+    }
+
+    private int getInetPort() {
+        int port = this.websocketUri.getPort();
+        return port == -1 ? "wss".equalsIgnoreCase(websocketUri.getScheme()) ? 443 : 80 : port;
+    }
+
+    @Override
+    public void connect(Runnable success, Consumer<Throwable> failed) {
+        if (this.cancelReconnect) {
+            this.cancelReconnect = false;
+        }
+        if (!checkStatus(ClientStatusEnums.INITIALIZED)) {
+            return;
+        }
+        if (getStatus() == ClientStatusEnums.CONNECTED) {
+            return;
+        }
+        if (getStatus() != ClientStatusEnums.RECONNECTING) {
+            this.setStatus(ClientStatusEnums.CONNECTING);
+        }
+        this.bootstrap.connect().addListener((ChannelFutureListener) connectFuture -> {
+            if (connectFuture.isSuccess()) {
+                if (log.isDebugEnabled()) {
+                    log.debug("连接建立成功!");
+                }
+                this.channel = connectFuture.channel();
+                // 监听是否握手成功
+                this.connectionHandler.getHandshakeFuture().addListener((ChannelFutureListener) handshakeFuture -> {
+                    try {
+                        connectionHandler.sendAuthRequest(channel);
+                        if (success != null) {
+                            success.run();
+                        }
+                    } catch (Exception e) {
+                        log.error("认证包发送失败,断开连接", e);
+                        this.disconnect();
+                    }
+                });
+            } else {
+                log.error("连接建立失败", connectFuture.cause());
+                this.onConnectFailed(this.connectionHandler);
+                if (failed != null) {
+                    failed.accept(connectFuture.cause());
+                }
+            }
+        });
+    }
+
+    @Override
+    public void disconnect() {
+        if (this.channel == null) {
+            return;
+        }
+        this.channel.close();
+    }
+
+    @Override
+    protected void tryReconnect() {
+        if (this.cancelReconnect) {
+            this.cancelReconnect = false;
+            return;
+        }
+        if (!getConfig().isAutoReconnect()) {
+            return;
+        }
+        if (log.isWarnEnabled()) {
+            log.warn("{}s后将重新连接 {}", getConfig().getReconnectDelay(), getConfig().getRoomId());
+        }
+        workerGroup.schedule(() -> {
+            this.setStatus(ClientStatusEnums.RECONNECTING);
+            this.connect();
+        }, getConfig().getReconnectDelay(), TimeUnit.SECONDS);
+    }
+
+    @Override
+    public void send(Object msg, Runnable success, Consumer<Throwable> failed) {
+        ChannelFuture future = this.channel.writeAndFlush(msg);
+        if (success != null || failed != null) {
+            future.addListener((ChannelFutureListener) channelFuture -> {
+                if (channelFuture.isSuccess()) {
+                    if (success != null) {
+                        success.run();
+                    }
+                } else {
+                    if (failed != null) {
+                        failed.accept(channelFuture.cause());
+                    }
+                }
+            });
+        }
+    }
+
+    @Override
+    public void destroy() {
+        super.destroy();
+
+        // 销毁时不需要重连
+        this.cancelReconnect = true;
+        workerGroup.shutdownGracefully().addListener(future -> {
+            if (future.isSuccess()) {
+                this.setStatus(ClientStatusEnums.DESTROYED);
+            } else {
+                throw new BaseException("client销毁失败", future.cause());
+            }
+        });
+    }
+
+    @Override
+    protected String getWebSocketUriString() {
+        return getConfig().getWebsocketUri();
+    }
+
+    @Override
+    protected void setStatus(ClientStatusEnums status) {
+        if (log.isDebugEnabled()) {
+            if (getStatus() != status) {
+                log.debug("{} 状态变化 {} => {}\n", getClass().getSimpleName(), getStatus(), status);
+            }
+        }
+        super.setStatus(status);
+    }
+
+    @Override
+    public void sendDanmu(Object danmu, Runnable success, Consumer<Throwable> failed) {
+        throw new BaseException("暂未支持该功能");
+    }
+
+    @Override
+    public void clickLike(int count, Runnable success, Consumer<Throwable> failed) {
+        throw new BaseException("暂未支持该功能");
+    }
+
+    /**
+     * 发送弹幕前判断是否可以发送
+     *
+     * @param checkConnected 是否检查Client连接状态
+     */
+    protected boolean checkCanSendDanmu(boolean checkConnected) {
+        if (checkConnected && getStatus() != ClientStatusEnums.CONNECTED) {
+            throw new BaseException("连接未建立,无法发送弹幕");
+        }
+        if (System.currentTimeMillis() - this.lastSendDanmuTimeInMillis <= getConfig().getMinSendDanmuPeriod()) {
+            if (log.isWarnEnabled()) {
+                log.warn("发送弹幕频率过快,忽略该次发送");
+            }
+            return false;
+        }
+        return true;
+    }
+
+    protected boolean checkCanSendDanmu() {
+        return checkCanSendDanmu(true);
+    }
+
+    /**
+     * 发送弹幕后调用该方法
+     */
+    protected void finishSendDanmu() {
+        this.lastSendDanmuTimeInMillis = System.currentTimeMillis();
+        if (log.isDebugEnabled()) {
+            log.debug("弹幕发送完成");
+        }
+    }
+
+    public void iteratorMsgListeners(Consumer<MsgListener> consumer) {
+        binaryFrameHandler.iteratorMsgListeners(consumer);
+    }
+}

+ 65 - 0
ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty-client/src/main/java/tech/ordinaryroad/live/chat/client/servers/netty/client/config/BaseNettyClientConfig.java

@@ -0,0 +1,65 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.servers.netty.client.config;
+
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
+import io.netty.handler.codec.http.websocketx.WebSocketVersion;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+import tech.ordinaryroad.live.chat.client.commons.client.config.BaseLiveChatClientConfig;
+
+import java.net.URI;
+
+/**
+ * @author mjz
+ * @date 2023/8/26
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@SuperBuilder(toBuilder = true)
+public abstract class BaseNettyClientConfig extends BaseLiveChatClientConfig {
+
+    /**
+     * 聚合器允许的最大消息体长度,默认 64*1024 byte
+     *
+     * @see HttpObjectAggregator#HttpObjectAggregator(int)
+     */
+    @Builder.Default
+    private int aggregatorMaxContentLength = 64 * 1024;
+
+    /**
+     * WebSocketClientHandshaker最大消息体长度,默认 64*1024 byte
+     *
+     * @see WebSocketClientHandshakerFactory#newHandshaker(URI, WebSocketVersion, String, boolean, HttpHeaders, int)
+     */
+    @Builder.Default
+    private int maxFramePayloadLength = 64 * 1024;
+}

+ 66 - 0
ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty-client/src/main/java/tech/ordinaryroad/live/chat/client/servers/netty/client/handler/BaseNettyClientBinaryFrameHandler.java

@@ -0,0 +1,66 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.servers.netty.client.handler;
+
+import lombok.Getter;
+import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseMsgListener;
+import tech.ordinaryroad.live.chat.client.commons.base.msg.IMsg;
+import tech.ordinaryroad.live.chat.client.servers.netty.client.base.BaseNettyClient;
+import tech.ordinaryroad.live.chat.client.servers.netty.handler.base.BaseBinaryFrameHandler;
+
+import java.util.List;
+
+/**
+ * BaseClientBinaryFrameHandler
+ *
+ * @author mjz
+ * @date 2023/8/30
+ */
+public abstract class BaseNettyClientBinaryFrameHandler<
+        Client extends BaseNettyClient<?, ?, ?, ?, ?, ?>,
+        BinaryFrameHandler extends BaseBinaryFrameHandler<BinaryFrameHandler, CmdEnum, Msg, MsgListener>,
+        CmdEnum extends Enum<CmdEnum>,
+        Msg extends IMsg,
+        MsgListener extends IBaseMsgListener<BinaryFrameHandler, CmdEnum>>
+        extends BaseBinaryFrameHandler<BinaryFrameHandler, CmdEnum, Msg, MsgListener> {
+
+    @Getter
+    protected final Client client;
+
+    public BaseNettyClientBinaryFrameHandler(List<MsgListener> msgListeners, Client client, long roomId) {
+        super(msgListeners, roomId);
+        this.client = client;
+    }
+
+    public BaseNettyClientBinaryFrameHandler(List<MsgListener> msgListeners, Client client) {
+        super(msgListeners, client.getConfig().getRoomId());
+        this.client = client;
+    }
+
+    public BaseNettyClientBinaryFrameHandler(List<MsgListener> msgListeners, long roomId) {
+        super(msgListeners, roomId);
+        this.client = null;
+    }
+}

+ 65 - 0
ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty-client/src/main/java/tech/ordinaryroad/live/chat/client/servers/netty/client/handler/BaseNettyClientConnectionHandler.java

@@ -0,0 +1,65 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2023 OrdinaryRoad
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package tech.ordinaryroad.live.chat.client.servers.netty.client.handler;
+
+import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
+import lombok.Getter;
+import tech.ordinaryroad.live.chat.client.commons.base.listener.IBaseConnectionListener;
+import tech.ordinaryroad.live.chat.client.servers.netty.client.base.BaseNettyClient;
+import tech.ordinaryroad.live.chat.client.servers.netty.handler.base.BaseConnectionHandler;
+
+/**
+ * BaseClientConnectionHandler
+ *
+ * @author mjz
+ * @date 2023/8/27
+ */
+public abstract class BaseNettyClientConnectionHandler<
+        Client extends BaseNettyClient<?, ?, ?, ?, ?, ?>,
+        ConnectionHandler extends BaseConnectionHandler<ConnectionHandler>>
+        extends BaseConnectionHandler<ConnectionHandler> {
+
+    @Getter
+    protected final Client client;
+
+    public BaseNettyClientConnectionHandler(WebSocketClientHandshaker handshaker, Client client, IBaseConnectionListener<ConnectionHandler> listener) {
+        super(handshaker, listener);
+        this.client = client;
+    }
+
+    public BaseNettyClientConnectionHandler(WebSocketClientHandshaker handshaker, Client client) {
+        this(handshaker, client, null);
+    }
+
+    public BaseNettyClientConnectionHandler(WebSocketClientHandshaker handshaker, IBaseConnectionListener<ConnectionHandler> listener) {
+        super(handshaker, listener);
+        this.client = null;
+    }
+
+    public BaseNettyClientConnectionHandler(WebSocketClientHandshaker handshaker, long roomId) {
+        super(handshaker, null);
+        this.client = null;
+    }
+}

+ 58 - 0
ruoyi-modules/ruoyi-live/live-chat-client-servers/live-chat-client-servers-netty/pom.xml

@@ -0,0 +1,58 @@
+<!--
+  ~ MIT License
+  ~
+  ~ Copyright (c) 2023 OrdinaryRoad
+  ~
+  ~ Permission is hereby granted, free of charge, to any person obtaining a copy
+  ~ of this software and associated documentation files (the "Software"), to deal
+  ~ in the Software without restriction, including without limitation the rights
+  ~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  ~ copies of the Software, and to permit persons to whom the Software is
+  ~ furnished to do so, subject to the following conditions:
+  ~
+  ~ The above copyright notice and this permission notice shall be included in all
+  ~ copies or substantial portions of the Software.
+  ~
+  ~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  ~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  ~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  ~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  ~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  ~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  ~ SOFTWARE.
+  -->
+
+<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>
+    <parent>
+        <groupId>com.xmzs</groupId>
+        <artifactId>live-chat-client-servers</artifactId>
+        <version>1.0.0</version>
+    </parent>
+    <packaging>jar</packaging>
+
+    <artifactId>live-chat-client-servers-netty</artifactId>
+    <name>live-chat-client-servers-netty</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.xmzs</groupId>
+            <artifactId>live-chat-client-commons-base</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+    </dependencies>
+</project>

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません