Przeglądaj źródła

fix : 客服聊天的webSocket

bilingfeng 1 rok temu
rodzic
commit
3a6245ba56
24 zmienionych plików z 814 dodań i 1 usunięć
  1. 5 0
      game-module/game-module-manage/pom.xml
  2. 49 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/config/KfMsgWebSocketConfig.java
  3. 5 1
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/constant/RedisKeyConstant.java
  4. 34 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfRoomMsgOwnerEnum.java
  5. 34 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfRoomMsgTypeEnum.java
  6. 72 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/AppletMsgDTO.java
  7. 48 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/KfUploadTempMediaDTO.java
  8. 9 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/rpc/impl/KfMsgRpcImpl.java
  9. 12 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfRoomMsgService.java
  10. 12 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfRoomService.java
  11. 12 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfSessionUserService.java
  12. 128 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfRoomMsgServiceImpl.java
  13. 18 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfRoomServiceImpl.java
  14. 18 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfSessionUserServiceImpl.java
  15. 39 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgRedisListener.java
  16. 29 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebSocketSessionRegistry.java
  17. 77 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebsocketHandler.java
  18. 2 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfLink.java
  19. 55 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfRoom.java
  20. 65 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfRoomMsg.java
  21. 55 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfSessionUser.java
  22. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfRoomMapper.java
  23. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfRoomMsgMapper.java
  24. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfSessionUserMapper.java

+ 5 - 0
game-module/game-module-manage/pom.xml

@@ -34,6 +34,11 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
+        <!-- SpringBoot websocket -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
         <!-- nacos配置中心 默认的 nacos-client 2.0.3有 bug -->
         <dependency>
             <groupId>com.alibaba.cloud</groupId>

+ 49 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/config/KfMsgWebSocketConfig.java

@@ -0,0 +1,49 @@
+package com.zanxiang.game.module.manage.config;
+
+import com.zanxiang.game.module.manage.constant.RedisKeyConstant;
+import com.zanxiang.game.module.manage.websocket.KfMsgRedisListener;
+import com.zanxiang.game.module.manage.websocket.KfMsgWebsocketHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.listener.PatternTopic;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服消息WebSocket配置
+ */
+@Configuration
+@EnableWebSocket
+public class KfMsgWebSocketConfig implements WebSocketConfigurer {
+
+    private final KfMsgWebsocketHandler kfMsgWebsocketHandler;
+
+    private final KfMsgRedisListener kfMsgRedisListener;
+
+    @Autowired
+    public KfMsgWebSocketConfig(KfMsgWebsocketHandler kfMsgWebsocketHandler, KfMsgRedisListener kfMsgRedisListener) {
+        this.kfMsgWebsocketHandler = kfMsgWebsocketHandler;
+        this.kfMsgRedisListener = kfMsgRedisListener;
+    }
+
+    @Override
+    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+        registry.addHandler(kfMsgWebsocketHandler, "/api/kf/msg").setAllowedOrigins("*");
+    }
+
+    @Bean
+    public RedisMessageListenerContainer redisContainer(RedisConnectionFactory connectionFactory) {
+        MessageListenerAdapter listenerAdapter = new MessageListenerAdapter(kfMsgRedisListener, "onMessage");
+        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
+        container.setConnectionFactory(connectionFactory);
+        container.addMessageListener(listenerAdapter, new PatternTopic(RedisKeyConstant.KF_MSG_REDIS_LISTEN_TOPIC));
+        return container;
+    }
+}

+ 5 - 1
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/constant/RedisKeyConstant.java

@@ -25,7 +25,11 @@ public class RedisKeyConstant {
     /**
      * 小程序失败计数器
      */
-    public static final String APPLET_ERROR_COUNT = RedisKeyConstant.REDIS_PREFIX + "APPLET_ERROR_COUNT_";
+    public static final String APPLET_ERROR_COUNT = RedisKeyConstant.REDIS_PREFIX + "applet_error_count_";
 
+    /**
+     * 客服消息redis监听器消息频道
+     */
+    public static final String KF_MSG_REDIS_LISTEN_TOPIC = RedisKeyConstant.REDIS_PREFIX + "kf_msg_channel";
 
 }

+ 34 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfRoomMsgOwnerEnum.java

@@ -0,0 +1,34 @@
+package com.zanxiang.game.module.manage.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服房间消息归属
+ */
+@Getter
+@AllArgsConstructor
+public enum KfRoomMsgOwnerEnum {
+
+    /**
+     * 系统消息
+     */
+    KF_MSG_OWNER_SYSTEM("KF_MSG_OWNER_SYSTEM"),
+
+    /**
+     * 玩家消息
+     */
+    KF_MSG_OWNER_USER("KF_MSG_OWNER_USER"),
+
+    /**
+     * 客服消息
+     */
+    KF_MSG_OWNER_KF("KF_MSG_OWNER_KF");
+
+    /**
+     * 消息类型
+     */
+    private String value;
+}

+ 34 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfRoomMsgTypeEnum.java

@@ -0,0 +1,34 @@
+package com.zanxiang.game.module.manage.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天消息类型
+ */
+@Getter
+@AllArgsConstructor
+public enum KfRoomMsgTypeEnum {
+
+    /**
+     * 文本消息
+     */
+    KF_MSG_TYPE_TEXT("text"),
+
+    /**
+     * 图片消息
+     */
+    KF_MSG_TYPE_IMAGE("image"),
+
+    /**
+     * 图文连接
+     */
+    KF_MSG_TYPE_LINK("link");
+
+    /**
+     * 消息类型
+     */
+    private String value;
+}

+ 72 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/AppletMsgDTO.java

@@ -0,0 +1,72 @@
+package com.zanxiang.game.module.manage.pojo.dto;
+
+import lombok.Data;
+
+/**
+ * @author : lingfeng
+ * @time : 2023-01-05
+ * @description : 小程序消息
+ */
+@Data
+public class AppletMsgDTO {
+
+    /**
+     * 回调事件消息
+     */
+    public static final String MSG_TYPE_EVENT = "event";
+
+    /**
+     * 用户进入会话事件
+     */
+    public static final String EVENT_USER_ENTER_TEMP_SESSION = "user_enter_tempsession";
+
+    /**
+     * 客服支付约定消息文本
+     */
+    public static final String MSG_CONTENT_PAY = "2";
+
+    /**
+     * 开发者微信号
+     */
+    private String ToUserName;
+
+    /**
+     * 发送方帐号(一个OpenID)
+     */
+    private String FromUserName;
+
+    /**
+     * 消息创建时间 (整型)
+     */
+    private String CreateTime;
+
+    /**
+     * 消息类型,文本为text
+     */
+    private String MsgType;
+
+    /**
+     * 事件类型
+     */
+    private String Event;
+
+    /**
+     * 文本消息内容
+     */
+    private String Content;
+
+    /**
+     * 消息id,64位整型
+     */
+    private Long MsgId;
+
+    /**
+     * 图片素材id
+     */
+    private String MediaId;
+
+    /**
+     * 图片地址
+     */
+    private String PicUrl;
+}

+ 48 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/KfUploadTempMediaDTO.java

@@ -0,0 +1,48 @@
+package com.zanxiang.game.module.manage.pojo.dto;
+
+import lombok.Data;
+
+import java.util.Objects;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服消息上传临时素材
+ */
+@Data
+public class KfUploadTempMediaDTO {
+
+    /**
+     * 请求返回成功
+     */
+    public static final long CODE_SUCCESS = 0;
+
+    /**
+     * 错误码, 成功返回 0, 40001 : access_token错误, 40004 : 不合法的媒体文件类型
+     */
+    private long errcode;
+
+    /**
+     * 错误消息
+     */
+    private String errmsg;
+
+    /**
+     * 素材类型
+     */
+    private String type;
+
+    /**
+     * 素材id
+     */
+    private String media_id;
+
+    /**
+     * 上传时间戳
+     */
+    private String created_at;
+
+    public boolean isSuccess() {
+        return Objects.equals(this.errcode, KfUploadTempMediaDTO.CODE_SUCCESS);
+    }
+}

+ 9 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/rpc/impl/KfMsgRpcImpl.java

@@ -0,0 +1,9 @@
+package com.zanxiang.game.module.manage.rpc.impl;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-26
+ * @description : 客服消息rpc接口
+ */
+public class KfMsgRpcImpl {
+}

+ 12 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfRoomMsgService.java

@@ -0,0 +1,12 @@
+package com.zanxiang.game.module.manage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zanxiang.game.module.mybatis.entity.KfRoomMsg;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服房间消息
+ */
+public interface IKfRoomMsgService extends IService<KfRoomMsg> {
+}

+ 12 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfRoomService.java

@@ -0,0 +1,12 @@
+package com.zanxiang.game.module.manage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zanxiang.game.module.mybatis.entity.KfRoom;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天房间
+ */
+public interface IKfRoomService extends IService<KfRoom> {
+}

+ 12 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfSessionUserService.java

@@ -0,0 +1,12 @@
+package com.zanxiang.game.module.manage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zanxiang.game.module.mybatis.entity.KfSessionUser;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天玩家信息
+ */
+public interface IKfSessionUserService extends IService<KfSessionUser> {
+}

+ 128 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfRoomMsgServiceImpl.java

@@ -0,0 +1,128 @@
+package com.zanxiang.game.module.manage.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zanxiang.game.module.base.ServerInfo;
+import com.zanxiang.game.module.base.pojo.enums.HttpStatusEnum;
+import com.zanxiang.game.module.base.rpc.IWxApiServiceRpc;
+import com.zanxiang.game.module.manage.pojo.dto.GameAppletDTO;
+import com.zanxiang.game.module.manage.pojo.dto.KfUploadTempMediaDTO;
+import com.zanxiang.game.module.manage.service.IKfRoomMsgService;
+import com.zanxiang.game.module.mybatis.entity.KfRoomMsg;
+import com.zanxiang.game.module.mybatis.mapper.KfRoomMsgMapper;
+import com.zanxiang.module.util.JsonUtil;
+import com.zanxiang.module.util.exception.BaseException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天房间消息
+ */
+@Slf4j
+@Service
+public class KfRoomMsgServiceImpl extends ServiceImpl<KfRoomMsgMapper, KfRoomMsg> implements IKfRoomMsgService {
+
+    @Autowired
+    private RestTemplate restTemplate;
+
+    @DubboReference(providedBy = ServerInfo.SERVER_SDK_DUBBO_NAME)
+    private IWxApiServiceRpc wxApiServiceRpc;
+
+    /**
+     * 通过腾讯API给玩家发送消息
+     */
+    private String sendCustomMessageApi(GameAppletDTO gameAppletDTO, String openId, String msgType, Map<String, Object> msgMap) {
+        //客服消息参数构造
+        Map<String, Object> paramMap = new HashMap<>(3);
+        paramMap.put("touser", openId);
+        paramMap.put("msgtype", msgType);
+        paramMap.put(msgType, msgMap);
+        log.error("客服消息发送参数, paramMap : {}", JsonUtil.toString(paramMap));
+        //获取接口token
+        String accessToken = wxApiServiceRpc.getAccessToken(gameAppletDTO.getAppId(), gameAppletDTO.getAppSecret());
+        URI uri = UriComponentsBuilder.fromHttpUrl("https://api.weixin.qq.com/cgi-bin/message/custom/send")
+                .queryParam("access_token", accessToken)
+                .build().toUri();
+        // 发送请求
+        String result = restTemplate.postForObject(uri, paramMap, String.class);
+        log.error("客服消息发送结果, result : {}", result);
+        return HttpStatusEnum.SUCCESS.getMsg();
+    }
+
+    /**
+     * 图片上传临时素材
+     */
+    private String uploadTempMedia(GameAppletDTO gameAppletDTO, String openId, String msgType, Map<String, Object> msgMap) {
+        //客服消息参数构造
+        Map<String, Object> paramMap = new HashMap<>(3);
+        paramMap.put("touser", openId);
+        paramMap.put("msgtype", msgType);
+        paramMap.put(msgType, msgMap);
+        log.error("客服消息发送参数, paramMap : {}", JsonUtil.toString(paramMap));
+        //获取接口token
+        String accessToken = wxApiServiceRpc.getAccessToken(gameAppletDTO.getAppId(), gameAppletDTO.getAppSecret());
+        URI uri = UriComponentsBuilder.fromHttpUrl("https://api.weixin.qq.com/cgi-bin/media/upload")
+                .queryParam("access_token", accessToken)
+                .build().toUri();
+        // 发送请求
+        String result = restTemplate.postForObject(uri, paramMap, String.class);
+        log.error("客服消息发送结果, result : {}", result);
+        return HttpStatusEnum.SUCCESS.getMsg();
+    }
+
+
+    /**
+     * 上传临时素材
+     */
+    public String mediaUpload(String accessToken, MultipartFile files) {
+        URI uri = UriComponentsBuilder.fromHttpUrl("https://api.weixin.qq.com/cgi-bin/media/upload")
+                .queryParam("access_token", accessToken)
+                .queryParam("type", "image")
+                .build().toUri();
+        //最外层请求头
+        HttpHeaders httpHeaders = new HttpHeaders();
+        httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
+        LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
+        //素材内容请求头
+        HttpHeaders formHeader = new HttpHeaders();
+        formHeader.setContentDispositionFormData("media", files.getOriginalFilename());
+        formHeader.setContentType(MediaType.MULTIPART_FORM_DATA);
+        HttpEntity<Resource> formEntity = new HttpEntity<>(files.getResource(), formHeader);
+        map.add("media", formEntity);
+        //http请求头
+        HttpEntity<LinkedMultiValueMap<String, Object>> httpEntity = new HttpEntity<>(map, httpHeaders);
+        Object result;
+        try {
+            result = restTemplate.postForObject(uri, httpEntity, Object.class);
+        } catch (Exception e) {
+            log.error("客服消息上传临时素材异常, e : {}", e.getMessage());
+            throw new BaseException("上传临时素材异常");
+        }
+        if (result == null) {
+//            log.error("企微上传临时素材异常, accessToken : {}, type : {}", accessToken, type);
+            throw new BaseException("企微上传临时素材返回结果为空");
+        }
+        KfUploadTempMediaDTO res = JsonUtil.toObj(JsonUtil.toString(result), KfUploadTempMediaDTO.class);
+        if (res.isSuccess()) {
+            return res.getMedia_id();
+        }
+//        log.error("企微上传临时素材异常, accessToken : {}, type : {}, errmsg : {}", accessToken, type, res.getErrmsg());
+        throw new BaseException("企微上传临时素材失败");
+    }
+
+}

+ 18 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfRoomServiceImpl.java

@@ -0,0 +1,18 @@
+package com.zanxiang.game.module.manage.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zanxiang.game.module.manage.service.IKfRoomService;
+import com.zanxiang.game.module.mybatis.entity.KfRoom;
+import com.zanxiang.game.module.mybatis.mapper.KfRoomMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天房间
+ */
+@Slf4j
+@Service
+public class KfRoomServiceImpl extends ServiceImpl<KfRoomMapper, KfRoom> implements IKfRoomService {
+}

+ 18 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfSessionUserServiceImpl.java

@@ -0,0 +1,18 @@
+package com.zanxiang.game.module.manage.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zanxiang.game.module.manage.service.IKfSessionUserService;
+import com.zanxiang.game.module.mybatis.entity.KfSessionUser;
+import com.zanxiang.game.module.mybatis.mapper.KfSessionUserMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天玩家信息
+ */
+@Slf4j
+@Service
+public class KfSessionUserServiceImpl extends ServiceImpl<KfSessionUserMapper, KfSessionUser> implements IKfSessionUserService {
+}

+ 39 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgRedisListener.java

@@ -0,0 +1,39 @@
+package com.zanxiang.game.module.manage.websocket;
+
+import com.zanxiang.erp.security.util.SecurityUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服消息redis监听器
+ */
+@Component
+public class KfMsgRedisListener implements MessageListener {
+
+    @Autowired
+    private KfMsgWebSocketSessionRegistry kfMsgWebSocketSessionRegistry;
+
+    @Override
+    public void onMessage(Message message, byte[] pattern) {
+        //从redis中拿到的消息
+        String messageBody = new String(message.getBody());
+        //从消息中获取userId
+        WebSocketSession session = kfMsgWebSocketSessionRegistry.getSession(SecurityUtil.getUserId().toString());
+        //判断连接存在, 且未来断开, 发送消息
+        if (session != null && session.isOpen()) {
+            try {
+                session.sendMessage(new TextMessage(messageBody));
+            } catch (IOException e) {
+                // Handle exception
+            }
+        }
+    }
+}

+ 29 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebSocketSessionRegistry.java

@@ -0,0 +1,29 @@
+package com.zanxiang.game.module.manage.websocket;
+
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服消息SocketSession管理
+ */
+@Component
+public class KfMsgWebSocketSessionRegistry {
+
+    private final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
+
+    public void addSession(String userId, WebSocketSession session) {
+        sessions.put(userId, session);
+    }
+
+    public WebSocketSession getSession(String userId) {
+        return sessions.get(userId);
+    }
+
+    public void removeSession(String userId) {
+        sessions.remove(userId);
+    }
+}

+ 77 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebsocketHandler.java

@@ -0,0 +1,77 @@
+package com.zanxiang.game.module.manage.websocket;
+
+import com.zanxiang.erp.security.util.SecurityUtil;
+import com.zanxiang.game.module.manage.constant.RedisKeyConstant;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服消息Websocket处理器
+ */
+@Slf4j
+@Lazy(value = false)
+@Component
+public class KfMsgWebsocketHandler implements WebSocketHandler {
+
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+
+    @Autowired
+    private KfMsgWebSocketSessionRegistry kfMsgWebSocketSessionRegistry;
+
+    /**
+     * websocket连接建立成功
+     */
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+        log.error("前端长连接建立成功");
+        kfMsgWebSocketSessionRegistry.addSession(SecurityUtil.getUserId().toString(), session);
+    }
+
+    /**
+     * 收到消息
+     */
+    @Override
+    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
+        String msgStr = message.getPayload().toString();
+        log.error("收到前端消息 msgStr : {}", msgStr);
+        this.pushMessage("你好!武哥");
+    }
+
+    /**
+     * 消息传输发生错误
+     */
+    @Override
+    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
+        log.error("发生错误");
+    }
+
+    /**
+     * 连接被关闭
+     */
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
+        log.error("连接被关闭");
+    }
+
+    @Override
+    public boolean supportsPartialMessages() {
+        return false;
+    }
+
+    /**
+     * 消息发送到redis广播
+     */
+    public void pushMessage(String message) {
+        redisTemplate.convertAndSend(RedisKeyConstant.KF_MSG_REDIS_LISTEN_TOPIC, message);
+    }
+}

+ 2 - 0
game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfLink.java

@@ -18,6 +18,8 @@ import java.time.LocalDateTime;
 @TableName("t_kf_link")
 public class KfLink {
 
+    private static final long serialVersionUID = 1L;
+
     /**
      * 应用id
      */

+ 55 - 0
game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfRoom.java

@@ -0,0 +1,55 @@
+package com.zanxiang.game.module.mybatis.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : ${description}
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+@Builder
+@TableName("t_kf_room")
+public class KfRoom {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 玩家open_id
+     */
+    private String openId;
+
+    /**
+     * 客服id
+     */
+    private Long kfUserId;
+
+    /**
+     * 房间在线状态
+     */
+    private Boolean online;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 65 - 0
game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfRoomMsg.java

@@ -0,0 +1,65 @@
+package com.zanxiang.game.module.mybatis.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : ${description}
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+@Builder
+@TableName("t_kf_room_msg")
+public class KfRoomMsg {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 消息id
+     */
+    @TableId(value = "msgId", type = IdType.INPUT)
+    private String msgId;
+
+    /**
+     * 消息类型
+     */
+    private String msgType;
+
+    /**
+     * 已读状态
+     */
+    private Boolean readStatus;
+
+    /**
+     * 房间id
+     */
+    private Long roomId;
+
+    /**
+     * 消息归属
+     */
+    private String msgOwner;
+
+    /**
+     * 消息内容
+     */
+    private String content;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 55 - 0
game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfSessionUser.java

@@ -0,0 +1,55 @@
+package com.zanxiang.game.module.mybatis.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDateTime;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : ${description}
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+@Builder
+@TableName("t_kf_session_user")
+public class KfSessionUser {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 玩家openId
+     */
+    @TableId(value = "openId", type = IdType.INPUT)
+    private String openId;
+
+    /**
+     * 玩家id
+     */
+    private Long userId;
+
+    /**
+     * 游戏id
+     */
+    private Long gameId;
+
+    /**
+     * 是否待接入
+     */
+    private Boolean isWait;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 12 - 0
game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfRoomMapper.java

@@ -0,0 +1,12 @@
+package com.zanxiang.game.module.mybatis.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zanxiang.game.module.mybatis.entity.KfRoom;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : ${description}
+ */
+public interface KfRoomMapper extends BaseMapper<KfRoom> {
+}

+ 12 - 0
game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfRoomMsgMapper.java

@@ -0,0 +1,12 @@
+package com.zanxiang.game.module.mybatis.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zanxiang.game.module.mybatis.entity.KfRoomMsg;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : ${description}
+ */
+public interface KfRoomMsgMapper extends BaseMapper<KfRoomMsg> {
+}

+ 12 - 0
game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfSessionUserMapper.java

@@ -0,0 +1,12 @@
+package com.zanxiang.game.module.mybatis.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zanxiang.game.module.mybatis.entity.KfSessionUser;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : ${description}
+ */
+public interface KfSessionUserMapper extends BaseMapper<KfSessionUser> {
+}