浏览代码

Merge remote-tracking branch 'origin/package-talk' into package

# Conflicts:
#	game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/ManageApplication.java
bilingfeng 1 年之前
父节点
当前提交
a693b24f55
共有 61 个文件被更改,包括 3777 次插入214 次删除
  1. 16 0
      game-module/game-module-base/src/main/java/com/zanxiang/game/module/base/rpc/IKfMsgRpc.java
  2. 15 0
      game-module/game-module-manage/pom.xml
  3. 1 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/ManageApplication.java
  4. 49 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/config/KfMsgWebSocketConfig.java
  5. 20 1
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/constant/RedisKeyConstant.java
  6. 1 1
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/ExcludeTagsEnum.java
  7. 39 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfOperateEnum.java
  8. 34 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfRoomMsgOwnerEnum.java
  9. 45 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfRoomMsgTypeEnum.java
  10. 89 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfWebSocketMsgEnum.java
  11. 44 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/OrderStateEnum.java
  12. 72 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/KfAppletMsgDTO.java
  13. 48 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/KfUploadTempMediaDTO.java
  14. 617 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/KfWebSocketMsgDTO.java
  15. 107 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/params/KfWebSocketMsgParam.java
  16. 29 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/rpc/impl/KfMsgRpcImpl.java
  17. 17 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfAppletMsgService.java
  18. 49 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfQuickReplyService.java
  19. 37 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfRoomMsgService.java
  20. 92 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfRoomService.java
  21. 23 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfSessionUserService.java
  22. 0 9
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IOssService.java
  23. 8 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IPayApplicationService.java
  24. 115 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/api/KfWxApiService.java
  25. 107 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/CpCallServiceImpl.java
  26. 363 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfAppletMsgServiceImpl.java
  27. 82 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfQuickReplyServiceImpl.java
  28. 91 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfRoomMsgServiceImpl.java
  29. 285 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfRoomServiceImpl.java
  30. 82 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfSessionUserServiceImpl.java
  31. 0 16
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/OssServiceImpl.java
  32. 16 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/PayApplicationServiceImpl.java
  33. 91 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/utils/FileUtil.java
  34. 64 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgRedisListener.java
  35. 35 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebSocketSessionRegistry.java
  36. 493 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebsocketHandler.java
  37. 56 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebsocketHeartbeat.java
  38. 5 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/Game.java
  39. 2 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfLink.java
  40. 55 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfQuickReply.java
  41. 65 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfRoom.java
  42. 91 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfRoomMsg.java
  43. 80 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfSessionUser.java
  44. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfQuickReplyMapper.java
  45. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfRoomMapper.java
  46. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfRoomMsgMapper.java
  47. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfSessionUserMapper.java
  48. 1 1
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/SDKApplication.java
  49. 5 0
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/constant/RedisKeyConstant.java
  50. 0 9
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/IGamePayStrategyService.java
  51. 10 0
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/IGamePayWayService.java
  52. 0 12
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/IKfLinkService.java
  53. 20 122
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GameAppletServiceImpl.java
  54. 0 19
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GamePayStrategyServiceImpl.java
  55. 13 0
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GamePayWayServiceImpl.java
  56. 2 1
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GameUserRoleServiceImpl.java
  57. 0 18
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/KfLinkServiceImpl.java
  58. 12 0
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/OrderPayServiceImpl.java
  59. 4 1
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/UserServiceImpl.java
  60. 1 1
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/pay/AliPayService.java
  61. 31 3
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/pay/WxPayService.java

+ 16 - 0
game-module/game-module-base/src/main/java/com/zanxiang/game/module/base/rpc/IKfMsgRpc.java

@@ -0,0 +1,16 @@
+package com.zanxiang.game.module.base.rpc;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-26
+ * @description : 客服消息rpc
+ */
+public interface IKfMsgRpc {
+
+    /**
+     * 小程序消息
+     *
+     * @param postData : 消息内容
+     */
+    void appletMsg(String postData);
+}

+ 15 - 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>
@@ -122,6 +127,16 @@
             <artifactId>springfox-swagger-ui</artifactId>
             <version>${swagger2.ui.version}</version>
         </dependency>
+        <dependency>
+            <groupId>commons-fileupload</groupId>
+            <artifactId>commons-fileupload</artifactId>
+            <version>1.3.1</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.4</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 1 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/ManageApplication.java

@@ -24,6 +24,7 @@ public class ManageApplication {
     public static void main(String[] args) {
         SpringApplication.run(ManageApplication.class, args);
         System.out.println("赞象Manage服务启动成功 <导量插入渠道变更记录, 修改手机号加限制> ( ´・・)ノ(._.`) \n" +
+        System.out.println("赞象Manage服务启动成功 <Websocket增加心跳机制01> ( ´・・)ノ(._.`) \n" +
                 "___  ___  ___   _   _   ___  _____  _____ \n" +
                 "|  \\/  | / _ \\ | \\ | | / _ \\|  __ \\|  ___|\n" +
                 "| .  . |/ /_\\ \\|  \\| |/ /_\\ \\ |  \\/| |__  \n" +

+ 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;
+    }
+}

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

@@ -25,7 +25,26 @@ 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";
+
+    /**
+     * 接入玩家线程锁
+     */
+    public static final String KF_MSG_USER_CONNECT_JOIN = RedisKeyConstant.REDIS_PREFIX + "kf_msg_user_connect_join_";
+
+    /**
+     * 客服支付订单标记
+     */
+    public static final String GAME_CUSTOM_PAY_SIGN = "game_sdk_manage_custom_pay_sign_";
+
+    /**
+     * 客服支付订单重新获取缓存
+     */
+    public static final String GAME_CUSTOM_PAY = "game_sdk_manage_custom_pay_";
 
 }

+ 1 - 1
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/ExcludeTagsEnum.java

@@ -34,7 +34,7 @@ public enum ExcludeTagsEnum {
 
     public static String getTagName(Integer tagID) {
         for (ExcludeTagsEnum excludeTagsEnum : ExcludeTagsEnum.values()) {
-            if (tagID == excludeTagsEnum.tagId) {
+            if (tagID.equals(excludeTagsEnum.tagId)) {
                 return excludeTagsEnum.getTagName();
             }
         }

+ 39 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfOperateEnum.java

@@ -0,0 +1,39 @@
+package com.zanxiang.game.module.manage.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-03-05
+ * @description : 快捷回复操作
+ */
+@Getter
+@AllArgsConstructor
+public enum KfOperateEnum {
+
+    /**
+     * 添加
+     */
+    KF_OPERATE_ADD("KF_OPERATE_ADD"),
+
+    /**
+     * 删除
+     */
+    KF_OPERATE_DELETE("KF_OPERATE_DELETE"),
+
+    /**
+     * 修改
+     */
+    KF_OPERATE_UPDATE("KF_OPERATE_UPDATE"),
+
+    /**
+     * 查询
+     */
+    KF_OPERATE_SELECT("KF_OPERATE_SELECT");
+
+    /**
+     * 交互类型
+     */
+    private String value;
+}

+ 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;
+}

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

@@ -0,0 +1,45 @@
+package com.zanxiang.game.module.manage.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * @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;
+
+    public static KfRoomMsgTypeEnum getMsgTypeEnum(String msgType) {
+        for (KfRoomMsgTypeEnum kfRoomMsgTypeEnum : KfRoomMsgTypeEnum.values()) {
+            if (Objects.equals(msgType, kfRoomMsgTypeEnum.getValue())) {
+                return kfRoomMsgTypeEnum;
+            }
+        }
+        return null;
+    }
+}

+ 89 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfWebSocketMsgEnum.java

@@ -0,0 +1,89 @@
+package com.zanxiang.game.module.manage.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-26
+ * @description : WebSocket通讯消息枚举
+ */
+@Getter
+@AllArgsConstructor
+public enum KfWebSocketMsgEnum {
+
+    /**
+     * 心跳
+     */
+    WEBSOCKET_MSG_CONNECT_HEART_BEAT("WEBSOCKET_MSG_CONNECT_HEART_BEAT"),
+
+    /**
+     * 握手, 接收前端消息, 返回游戏列表
+     */
+    WEBSOCKET_MSG_KF_HAND_SHAKE("WEBSOCKET_MSG_KF_HAND_SHAKE"),
+
+    /**
+     * 建立连接, 接收前端消息, 待接入列表, 已接入房间列表
+     */
+    WEBSOCKET_MSG_KF_CREATE_CONNECT("WEBSOCKET_MSG_KF_CREATE_CONNECT"),
+
+    /**
+     * 玩家待接入, 主动推送玩家待接入列表
+     */
+    WEBSOCKET_MSG_WAIT_LIST("WEBSOCKET_MSG_WAIT_LIST"),
+
+    /**
+     * 玩家被接入, 接收前端消息, 直接返回完整已接入房间列表, 全部客服重新推送完整待接入列表, 消息类型 : WEBSOCKET_MSG_WAIT_LIST
+     */
+    WEBSOCKET_MSG_USER_CONNECT_JOIN("WEBSOCKET_MSG_USER_CONNECT_JOIN"),
+
+    /**
+     * 房间历史消息, 接收前端消息, 前端必须分页获取, 需要携带分页参数
+     */
+    WEBSOCKET_MSG_ROOM_HISTORY("WEBSOCKET_MSG_ROOM_HISTORY"),
+
+    /**
+     * 客服发送消息, 接收前端消息, 返回发送结果
+     */
+    WEBSOCKET_MSG_KF_SEND("WEBSOCKET_MSG_KF_SEND"),
+
+    /**
+     * 房间消息, 主动推送玩家消息
+     */
+    WEBSOCKET_MSG_ROOM_MSG("WEBSOCKET_MSG_ROOM_MSG"),
+
+    /**
+     * 获取已结束房间列表, 接收前端消息, 必须分页获取, 需要携带分页参数
+     */
+    WEBSOCKET_MSG_FINISH_ROOM_LIST("WEBSOCKET_MSG_FINISH_ROOM_LIST"),
+
+    /**
+     * 结束会话, 接收前端消息, 推送完整的已连接房间列表
+     */
+    WEBSOCKET_MSG_FINISH_SESSION("WEBSOCKET_MSG_FINISH_SESSION"),
+
+    /**
+     * 获取玩家信息
+     */
+    WEBSOCKET_MSG_GET_USER("WEBSOCKET_MSG_GET_USER"),
+
+    /**
+     * 获取角色列表
+     */
+    WEBSOCKET_MSG_GET_ROLE_LIST("WEBSOCKET_MSG_GET_ROLE_LIST"),
+
+    /**
+     * 获取订单列表
+     */
+    WEBSOCKET_MSG_GET_ORDER_LIST("WEBSOCKET_MSG_GET_ORDER_LIST"),
+
+    /**
+     * 快捷回复, 接收前端消息,请求动作包含查询, 修改, 删除, 新增
+     */
+    WEBSOCKET_MSG_QUICK_REPLY("WEBSOCKET_MSG_QUICK_REPLY");
+
+    /**
+     * 消息类型
+     */
+    private String value;
+}

+ 44 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/OrderStateEnum.java

@@ -0,0 +1,44 @@
+package com.zanxiang.game.module.manage.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author : lingfeng
+ * @time : 2022-06-07
+ * @description : 用户状态
+ */
+@Getter
+@AllArgsConstructor
+public enum OrderStateEnum {
+
+    /**
+     * 预下单
+     */
+    READY_PAY(0, "预下单"),
+
+    /**
+     * 待支付
+     */
+    WAIT_PAY(1, "待支付"),
+
+    /**
+     * 支付成功
+     */
+    SUCCESS_PAY(2, "支付成功"),
+
+    /**
+     * 订单关闭
+     */
+    CANCEL_PAY(-1, "订单关闭");
+
+    /**
+     * 状态
+     */
+    private final Integer code;
+
+    /**
+     * 描述
+     */
+    private final String msg;
+}

+ 72 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/KfAppletMsgDTO.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 KfAppletMsgDTO {
+
+    /**
+     * 回调事件消息
+     */
+    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 = 0L;
+
+    /**
+     * 错误码, 成功返回 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 this.errcode == null || Objects.equals(this.errcode, KfUploadTempMediaDTO.CODE_SUCCESS);
+    }
+}

+ 617 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/KfWebSocketMsgDTO.java

@@ -0,0 +1,617 @@
+package com.zanxiang.game.module.manage.pojo.dto;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zanxiang.erp.security.util.SecurityUtil;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgTypeEnum;
+import com.zanxiang.game.module.manage.enums.KfWebSocketMsgEnum;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-26
+ * @description : WebSocket客服消息通信返回
+ */
+@Data
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class KfWebSocketMsgDTO {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * webSocket消息类型
+     */
+    private KfWebSocketMsgEnum webSocketMsgType;
+
+    /**
+     * 客服id, 存在 : 表示该消息发送给指定客服, 不存在 : 表示该消息发送给所有在线客服
+     */
+    private Long kfUserId;
+
+    /**
+     * 分页信息
+     */
+    private PageBean page;
+
+    /**
+     * 游戏id
+     */
+    private Long gameId;
+
+    /**
+     * 房间id
+     */
+    private Long roomId;
+
+    /**
+     * 结果信息
+     */
+    private ResultBean result;
+
+    /**
+     * 游戏列表, 全量不分页
+     */
+    private List<GameBean> gameList;
+
+    /**
+     * 待接入列表, 全量不分页
+     */
+    private List<WaitUserBean> waitUserList;
+
+    /**
+     * 房间列表, 已接入房间全量返回, 已经结束房间分页返回, 配套返回 page 信息
+     */
+    private List<RoomBean> roomList;
+
+    /**
+     * 房间消息 : 房间消息推送和历史消息列表都用该字段, 历史消息列表会配套返回 page 信息
+     */
+    private List<RoomMsgBean> roomMsgList;
+
+    /**
+     * 玩家信息
+     */
+    private UserBean user;
+
+    /**
+     * 角色列表
+     */
+    private List<GameRoleBean> roleList;
+
+    /**
+     * 订单列表
+     */
+    private List<OrderBean> orderList;
+
+    /**
+     * 快捷回复列表
+     */
+    private List<QuickReplyBean> quickReplyList;
+
+    public KfWebSocketMsgDTO(KfWebSocketMsgEnum webSocketMsgType, Integer errorCode, String errorMsg) {
+        this.webSocketMsgType = webSocketMsgType;
+        this.kfUserId = SecurityUtil.getUserId();
+        this.result = ResultBean.builder()
+                .errorCode(errorCode)
+                .errorMsg(errorMsg)
+                .build();
+    }
+
+    public static KfWebSocketMsgDTO ok(KfWebSocketMsgEnum webSocketMsgType) {
+        return new KfWebSocketMsgDTO(webSocketMsgType, 0, "success");
+    }
+
+    public static KfWebSocketMsgDTO fail(KfWebSocketMsgEnum webSocketMsgType, String errorMsg) {
+        return new KfWebSocketMsgDTO(webSocketMsgType, 400, errorMsg);
+    }
+
+    public static KfWebSocketMsgDTO.PageBean transformPage(Page<?> pageBean) {
+        return KfWebSocketMsgDTO.PageBean.builder()
+                .pageNum(pageBean.getCurrent())
+                .pageSize(pageBean.getSize())
+                .pageTotal(pageBean.getPages())
+                .total(pageBean.getTotal())
+                .build();
+    }
+
+    public static KfWebSocketMsgDTO.PageBean defaultPage(Long pageNum, Long pageSice) {
+        return KfWebSocketMsgDTO.PageBean.builder()
+                .pageNum(pageNum)
+                .pageSize(pageSice)
+                .pageTotal(0L)
+                .total(0L)
+                .build();
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class QuickReplyBean {
+
+        /**
+         * 主键id
+         */
+        private Long id;
+
+        /**
+         * 内容
+         */
+        private String content;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class UserBean {
+
+        /**
+         * 玩家id
+         */
+        private Long userId;
+
+        /**
+         * 最近角色id
+         */
+        private String lastRoleId;
+
+        /**
+         * 最近角色名称
+         */
+        private String lastRoleName;
+
+        /**
+         * 角色服务器id
+         */
+        private String serverId;
+
+        /**
+         * 角色服务器名称
+         */
+        private String serverName;
+
+        /**
+         * 充值笔数
+         */
+        private Integer orderCount;
+
+        /**
+         * 最大充值金额
+         */
+        private BigDecimal orderMaxAmount;
+
+        /**
+         * 累计充值金额
+         */
+        private BigDecimal orderAmountSum;
+
+        /**
+         * 最后充值时间
+         */
+        private LocalDateTime lastPayTime;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class GameRoleBean {
+
+        /**
+         * 玩家id
+         */
+        private Long userId;
+
+        /**
+         * 最近角色id
+         */
+        private String roleId;
+
+        /**
+         * 最近角色名称
+         */
+        private String roleName;
+
+        /**
+         * 角色服务器id
+         */
+        private String serverId;
+
+        /**
+         * 角色服务器名称
+         */
+        private String serverName;
+
+        /**
+         * 角色等级
+         */
+        private Long roleLevel;
+
+        /**
+         * 玩家角色战力
+         */
+        private Long rolePower;
+
+        /**
+         * 角色创建时间
+         */
+        private LocalDateTime createTime;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class OrderBean {
+
+        /**
+         * 玩家id
+         */
+        private Long userId;
+
+        /**
+         * 订单id
+         */
+        private String orderId;
+
+        /**
+         * 角色id
+         */
+        private String roleId;
+
+        /**
+         * 角色名称
+         */
+        private String roleName;
+
+        /**
+         * 角色服务器id
+         */
+        private String serverId;
+
+        /**
+         * 角色服务器名称
+         */
+        private String serverName;
+
+        /**
+         * 游戏商品名称
+         */
+        private String productName;
+
+        /**
+         * 充值金额
+         */
+        private BigDecimal amount;
+
+        /**
+         * 下单时间
+         */
+        private LocalDateTime createTime;
+
+        /**
+         * 支付时间
+         */
+        private LocalDateTime payTime;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class GameBean {
+
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 游戏id
+         */
+        private Long gameId;
+
+        /**
+         * 小程序名称
+         */
+        private String appName;
+
+        /**
+         * 未读消息总数
+         */
+        private Integer unReadMsgCount;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class WaitUserBean {
+
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 玩家openId
+         */
+        private String openId;
+
+        /**
+         * 消息游戏id
+         */
+        private Long gameId;
+
+        /**
+         * 最近角色id
+         */
+        private String lastRoleId;
+
+        /**
+         * 最近角色名称
+         */
+        private String lastRoleName;
+
+        /**
+         * 未读消息列表
+         */
+        private List<WaitUserMsgBean> waitUserMsgList;
+
+        /**
+         * 未读消息总数
+         */
+        private Long unReadMsgCount;
+
+        /**
+         * 开始等待时间
+         */
+        private LocalDateTime waitStartTime;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class WaitUserMsgBean {
+
+        /**
+         * 消息id
+         */
+        private String msgId;
+
+        /**
+         * 消息类型
+         */
+        private String msgType;
+
+        /**
+         * 创建时间
+         */
+        private LocalDateTime createTime;
+
+        /**
+         * 消息内容
+         */
+        private String content;
+
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RoomBean {
+
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 房间id
+         */
+        private Long roomId;
+
+        /**
+         * 消息游戏id
+         */
+        private Long gameId;
+
+        /**
+         * 玩家open_id
+         */
+        private String openId;
+
+        /**
+         * 最近角色id
+         */
+        private String lastRoleId;
+
+        /**
+         * 最近角色名称
+         */
+        private String lastRoleName;
+
+        /**
+         * 客服id
+         */
+        private Long kfUserId;
+
+        /**
+         * 房间在线状态
+         */
+        private Boolean online;
+
+        /**
+         * 消息归属
+         */
+        private String msgOwner;
+
+        /**
+         * 最后一条消息
+         */
+        private String lastMsg;
+
+        /**
+         * 最后一条消息时间
+         */
+        private LocalDateTime lastMsgTime;
+
+        /**
+         * 未读消息总数
+         */
+        private Integer unReadMsgCount;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class RoomMsgBean {
+
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 消息id
+         */
+        private String msgId;
+
+        /**
+         * 消息类型
+         */
+        private KfRoomMsgTypeEnum msgType;
+
+        /**
+         * 消息游戏id
+         */
+        private Long gameId;
+
+        /**
+         * 房间id
+         */
+        private Long roomId;
+
+        /**
+         * 已读状态
+         */
+        private Boolean readStatus;
+
+        /**
+         * 消息归属
+         */
+        private String msgOwner;
+
+        /**
+         * 消息内容
+         */
+        private MsgContentBean content;
+
+        /**
+         * 创建时间
+         */
+        private LocalDateTime createTime;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class MsgContentBean {
+
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 文本内容
+         */
+        private String text;
+
+        /**
+         * 图片地址
+         */
+        private String image;
+
+        /**
+         * 图文消息
+         */
+        private LinkBean link;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class LinkBean {
+
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 标题
+         */
+        private String title;
+
+        /**
+         * 描述
+         */
+        private String description;
+
+        /**
+         * 跳转链接
+         */
+        private String url;
+
+        /**
+         * 图片地址
+         */
+        private String thumbUrl;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class PageBean {
+
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 当前页数
+         */
+        private Long pageNum;
+
+        /**
+         * 单页数量
+         */
+        private Long pageSize;
+
+        /**
+         * 总页数
+         */
+        private Long pageTotal;
+
+        /**
+         * 数据总量
+         */
+        private Long total;
+    }
+
+    @Data
+    @Builder
+    @AllArgsConstructor
+    @NoArgsConstructor
+    public static class ResultBean {
+
+        private static final long serialVersionUID = 1L;
+
+        /**
+         * 0 : 成功, 其他失败
+         */
+        private Integer errorCode;
+
+        /**
+         * 错误消息
+         */
+        private String errorMsg;
+    }
+
+}

+ 107 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/params/KfWebSocketMsgParam.java

@@ -0,0 +1,107 @@
+package com.zanxiang.game.module.manage.pojo.params;
+
+import com.zanxiang.game.module.manage.enums.KfOperateEnum;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgTypeEnum;
+import com.zanxiang.game.module.manage.enums.KfWebSocketMsgEnum;
+import lombok.Data;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-27
+ * @description : WebSocket客服消息通信参数
+ */
+@Data
+public class KfWebSocketMsgParam {
+
+    /**
+     * webSocket消息类型, 必传参数
+     */
+    private KfWebSocketMsgEnum webSocketMsgType;
+
+    /**
+     * 客服登录token, 必传参数
+     */
+    private String token;
+
+    /**
+     * 游戏id, 必传参数(首次握手除外)
+     */
+    private Long gameId;
+
+    /**
+     * 分页信息, 分页获取数据的时候, 必须传
+     */
+    private PageBean page;
+
+    /**
+     * 玩家id, 玩家接入的时候必须传, 查询玩家信息的时候必须传
+     */
+    private String openId;
+
+    /**
+     * 房间id, 获取房间历史消息, 发消息的时候 -> 必传
+     */
+    private Long roomId;
+
+    /**
+     * 消息内容, 客服发送消息的时候必传
+     */
+    private MsgContentBean msgContent;
+
+    /**
+     * 快捷回复
+     */
+    private QuickReplyBean quickReplyBean;
+
+    @Data
+    public static class QuickReplyBean {
+
+        /**
+         * 交互类型, 必传
+         */
+        private KfOperateEnum kfOperateEnum;
+
+        /**
+         * 主键id, 删除和修改的时候必传
+         */
+        private Long id;
+
+        /**
+         * 内容, 添加和修改的时候必传
+         */
+        private String content;
+    }
+
+    @Data
+    public static class MsgContentBean {
+
+        /**
+         * 消息类型, 文本传 : text, 图片传 : image
+         */
+        private KfRoomMsgTypeEnum msgType;
+
+        /**
+         * 文本内容
+         */
+        private String text;
+
+        /**
+         * 图片地址(oss下载地址)
+         */
+        private String image;
+    }
+
+    @Data
+    public static class PageBean {
+
+        /**
+         * 当前页数
+         */
+        private Long pageNum = 1L;
+
+        /**
+         * 单页数量
+         */
+        private Long pageSize = 10L;
+    }
+}

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

@@ -0,0 +1,29 @@
+package com.zanxiang.game.module.manage.rpc.impl;
+
+import com.zanxiang.game.module.base.rpc.IKfMsgRpc;
+import com.zanxiang.game.module.manage.service.IKfAppletMsgService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-26
+ * @description : 客服消息rpc接口
+ */
+@Slf4j
+@DubboService
+public class KfMsgRpcImpl implements IKfMsgRpc {
+
+    @Autowired
+    private IKfAppletMsgService kfAppletMsgService;
+
+    @Override
+    public void appletMsg(String postData) {
+        try {
+            kfAppletMsgService.appletMsg(postData);
+        } catch (Exception e) {
+            log.error("消费SDK转发的小程序消息异常, postData : {}", postData);
+        }
+    }
+}

+ 17 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfAppletMsgService.java

@@ -0,0 +1,17 @@
+package com.zanxiang.game.module.manage.service;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-28
+ * @description : 小程序客服消息处理
+ */
+public interface IKfAppletMsgService {
+
+    /**
+     * 接收到SDK转发的小程序消息
+     *
+     * @param postData : 消息内容
+     */
+    void appletMsg(String postData);
+
+}

+ 49 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfQuickReplyService.java

@@ -0,0 +1,49 @@
+package com.zanxiang.game.module.manage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.game.module.manage.pojo.params.KfWebSocketMsgParam;
+import com.zanxiang.game.module.mybatis.entity.KfQuickReply;
+import reactor.util.function.Tuple2;
+
+import java.util.List;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-03-05
+ * @description : 客服快捷回复
+ */
+public interface IKfQuickReplyService extends IService<KfQuickReply> {
+
+    /**
+     * 添加快捷回复
+     *
+     * @param param : 参数
+     * @return : 返回结果
+     */
+    boolean quickReplyAdd(KfWebSocketMsgParam.QuickReplyBean param);
+
+    /**
+     * 删除快捷回复
+     *
+     * @param param : 参数
+     * @return : 返回结果
+     */
+    boolean quickReplyDelete(KfWebSocketMsgParam.QuickReplyBean param);
+
+    /**
+     * 更新快捷回复
+     *
+     * @param param : 参数
+     * @return : 返回结果
+     */
+    boolean quickReplyUpdate(KfWebSocketMsgParam.QuickReplyBean param);
+
+    /**
+     * 分页查询
+     *
+     * @param pageBean : 分页参数
+     * @return : 返回
+     */
+    Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.QuickReplyBean>> quickReplyList(KfWebSocketMsgParam.PageBean pageBean);
+}

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

@@ -0,0 +1,37 @@
+package com.zanxiang.game.module.manage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.game.module.manage.pojo.params.KfWebSocketMsgParam;
+import com.zanxiang.game.module.mybatis.entity.KfRoom;
+import com.zanxiang.game.module.mybatis.entity.KfRoomMsg;
+import reactor.util.function.Tuple2;
+
+import java.util.List;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服房间消息
+ */
+public interface IKfRoomMsgService extends IService<KfRoomMsg> {
+
+    /**
+     * 分页获取房间历史消息
+     *
+     * @param roomId   : 房间id
+     * @param pageBean : 分页条件
+     * @return : 返回分页数据
+     */
+    Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.RoomMsgBean>> msgRoomHistory(Long roomId, KfWebSocketMsgParam.PageBean pageBean);
+
+    /**
+     * 发送消息保存
+     *
+     * @param gameId     : 游戏id
+     * @param kfRoom     : 房间
+     * @param msgContent : 消息内容
+     * @return : 返回保存结果
+     */
+    boolean sendMsgSave(Long gameId, KfRoom kfRoom, KfWebSocketMsgParam.MsgContentBean msgContent);
+}

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

@@ -0,0 +1,92 @@
+package com.zanxiang.game.module.manage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.game.module.manage.pojo.params.KfWebSocketMsgParam;
+import com.zanxiang.game.module.mybatis.entity.KfRoom;
+import reactor.util.function.Tuple2;
+
+import java.util.List;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天房间
+ */
+public interface IKfRoomService extends IService<KfRoom> {
+
+    /**
+     * 分页获取已结束房间列表
+     *
+     * @param gameId   : 游戏id
+     * @param pageBean : 分页信息
+     * @return : 返回分页数据
+     */
+    Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.RoomBean>> getFinishRoomList(Long gameId, KfWebSocketMsgParam.PageBean pageBean);
+
+    /**
+     * 玩家加入房间
+     *
+     * @param openId : 玩家openId
+     * @param gameId : 游戏id
+     * @return : 返回关联房间id
+     */
+    Long userJoinRoom(String openId, Long gameId);
+
+    /**
+     * 客服获取游戏列表
+     *
+     * @return : 返回游戏列表
+     */
+    List<KfWebSocketMsgDTO.GameBean> getKfGameList();
+
+    /**
+     * 根据游戏获取游戏信息
+     *
+     * @param gameId : 游戏id
+     * @return : 返回游戏信息
+     */
+    List<KfWebSocketMsgDTO.GameBean> getKfGameByGameId(Long gameId);
+
+    /**
+     * 待接入房间列表
+     *
+     * @param gameId : 游戏id
+     * @return : 返回待接入房间列表
+     */
+    List<KfWebSocketMsgDTO.RoomBean> getOnlineRoomList(Long gameId);
+
+    /**
+     * 根据房间id获取房间信息
+     *
+     * @param roomId : 房间id
+     * @return : 返回房间信息
+     */
+    List<KfWebSocketMsgDTO.RoomBean> getRoomByRoomId(Long roomId);
+
+    /**
+     * 获取玩家信息
+     *
+     * @param openId : 玩家openId
+     * @return : 返回玩家信息
+     */
+    KfWebSocketMsgDTO.UserBean getUserBean(String openId);
+
+    /**
+     * 分页获取玩家角色列表
+     *
+     * @param openId   : 玩家openId
+     * @param pageBean : 分页信息
+     * @return : 返回单页数据
+     */
+    Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.GameRoleBean>> getRoleBeanList(String openId, KfWebSocketMsgParam.PageBean pageBean);
+
+    /**
+     * 分页获取玩家订单列表
+     *
+     * @param openId   : 玩家openId
+     * @param pageBean : 分页信息
+     * @return : 返回单页数据
+     */
+    Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.OrderBean>> getOrderBeanList(String openId, KfWebSocketMsgParam.PageBean pageBean);
+}

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

@@ -0,0 +1,23 @@
+package com.zanxiang.game.module.manage.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.game.module.mybatis.entity.KfSessionUser;
+
+import java.util.List;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天玩家信息
+ */
+public interface IKfSessionUserService extends IService<KfSessionUser> {
+
+    /**
+     * 获取待接入列表
+     *
+     * @param gameId : 游戏id
+     * @return : 返回待接入列表
+     */
+    List<KfWebSocketMsgDTO.WaitUserBean> getWaitUserList(Long gameId);
+}

+ 0 - 9
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IOssService.java

@@ -1,9 +0,0 @@
-package com.zanxiang.game.module.manage.service;
-
-/**
- * @author : lingfeng
- * @time : 2022-07-12
- * @description : 阿里云oss
- */
-public interface IOssService {
-}

+ 8 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IPayApplicationService.java

@@ -91,4 +91,12 @@ public interface IPayApplicationService extends IService<PayApplication> {
      * @return {@link PayApplicationDTO}
      */
     PayApplicationDTO getByAppId(String appId);
+
+    /**
+     * 根据支付盒子id获取支付用用
+     *
+     * @param payBoxId : 支付盒子id
+     * @return : 返回支付应用
+     */
+    PayApplicationDTO getPayApplicationByPayBoxId(Long payBoxId);
 }

+ 115 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/api/KfWxApiService.java

@@ -0,0 +1,115 @@
+package com.zanxiang.game.module.manage.service.api;
+
+import com.zanxiang.game.module.base.ServerInfo;
+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.IGameAppletService;
+import com.zanxiang.module.util.JsonUtil;
+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 reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
+
+import java.net.URI;
+import java.util.Map;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-26
+ * @description : 客服聊天腾讯API接口
+ */
+@Slf4j
+@Service
+public class KfWxApiService {
+
+    @Autowired
+    private RestTemplate restTemplate;
+
+    @DubboReference(providedBy = ServerInfo.SERVER_SDK_DUBBO_NAME)
+    private IWxApiServiceRpc wxApiServiceRpc;
+
+    @Autowired
+    private IGameAppletService gameAppletService;
+
+    /**
+     * 通过腾讯API给玩家发送消息
+     */
+    public Tuple2<Long, String> sendCustomMessageApi(Long gameId, Map<String, Object> msgParamMap) {
+        GameAppletDTO gameAppletDTO = gameAppletService.getByGameId(gameId);
+        if (gameAppletDTO == null) {
+            return Tuples.of(400L, "小程序信息不存在");
+        }
+        //客服消息参数构造
+        log.error("客服消息发送参数, paramMap : {}", JsonUtil.toString(msgParamMap));
+        //获取接口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;
+        try {
+            result = restTemplate.postForObject(uri, msgParamMap, String.class);
+        } catch (Exception e) {
+            log.error("客服消息发送异常, e : {}", e.getMessage());
+            return Tuples.of(400L, e.getMessage());
+        }
+        log.error("客服消息发送结果, result : {}", result);
+        Map<String, String> resultMap = JsonUtil.toMap(result, Map.class, String.class);
+        return Tuples.of(Long.valueOf(resultMap.get("errcode")), resultMap.get("errmsg"));
+    }
+
+    /**
+     * 上传临时素材
+     */
+    public Tuple2<Long, String> mediaUpload(Long gameId, MultipartFile files) {
+        GameAppletDTO gameAppletDTO = gameAppletService.getByGameId(gameId);
+        if (gameAppletDTO == null) {
+            return Tuples.of(400L, "小程序信息不存在");
+        }
+        //客服消息参数构造
+        log.error("客服消息发送参数, gameId : {}", gameId);
+        //获取接口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)
+                .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);
+        String result;
+        try {
+            result = restTemplate.postForObject(uri, httpEntity, String.class);
+        } catch (Exception e) {
+            log.error("客服消息上传临时素材异常, e : {}", e.getMessage());
+            return Tuples.of(400L, e.getMessage());
+        }
+        log.error("客服消息上传临时素材结果, result : {}", result);
+        KfUploadTempMediaDTO resultDTO = JsonUtil.toObj(result, KfUploadTempMediaDTO.class);
+        log.error("客服消息上传临时素材结果, resultDTO : {}", resultDTO);
+        //返回
+        return resultDTO.isSuccess() ? Tuples.of(KfUploadTempMediaDTO.CODE_SUCCESS, resultDTO.getMedia_id())
+                : Tuples.of(resultDTO.getErrcode(), resultDTO.getErrmsg());
+    }
+}

+ 107 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/CpCallServiceImpl.java

@@ -0,0 +1,107 @@
+package com.zanxiang.game.module.manage.service.impl;
+
+import com.zanxiang.module.util.JsonUtil;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.web.client.RestTemplate;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-22
+ * @description : CP接口交互
+ */
+public class CpCallServiceImpl {
+
+    /**
+     * MD5加密
+     */
+    private static final String SIGN_MD5 = "MD5";
+
+//    public static void main(String[] args) throws Exception {
+//        test();
+//    }
+
+    public static void test() throws Exception {
+        String key = "355b7f07125c1ef71cfd10166e0b90aa";
+        RestTemplate restTemplate = new RestTemplate();
+
+        String url = "https://ht.lttx.t5yx.cn/extapi?action=BgzszhSendTip";
+
+        Map<String, Object> param = new HashMap<>();
+
+        String msgId = "testMsgId2";
+        param.put("msgId", msgId);
+
+        String strRan = "strRan1";
+        param.put("strRan", strRan);
+
+        Long time = 1709002794L;
+        param.put("time", time);
+
+        String signStr = "key=355b7f07125c1ef71cfd10166e0b90aa&msgId=" + msgId + "&strRan=" + strRan + "&time=" + time;
+
+        System.out.println("加密字符串 : " + signStr);
+
+
+        param.put("sign", CpCallServiceImpl.MD5(signStr));
+
+//        param.put("serverid", 226);
+        param.put("serverid", 592);
+        List<String> roleIds = new ArrayList<>();
+//        roleIds.add("798136461189027272");
+        roleIds.add("815728771283100062");
+        param.put("roleIds", roleIds);
+
+        param.put("pushType", 1);
+
+        String text = "尊敬的尊享玩家“角色名”:\n" +
+                "叮,尊享管家小诗正在微信上等待与您的见面,根据您的游戏角色成长,小诗特意为您定制一份战力快速升级的攻略和几个尊享限定礼包,助您快速提升~\n" +
+                "您要尽快通过游戏内“联系客服”按钮与我联系,时间有限,请您尽快与我联系哦。\n" +
+                "联系时请您带上此页面截图,同时请您保密勿将此内容分享给其他玩家,可能会导致尊享限定礼包被冒领哦!";
+        Map<String, Object> msgContent = new HashMap<>();
+        msgContent.put("text", text);
+
+        List<String> imgs = new ArrayList<>();
+        //图片地址弄一张最小的, CP方不会用, 有默认底图
+        imgs.add("https://test.84game.cn/1709024863.jpeg");
+        msgContent.put("imgs", JsonUtil.toString(imgs));
+
+        param.put("msgContent", msgContent);
+
+        System.out.println("接口参数 param :" + JsonUtil.toString(param));
+
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.set(HttpHeaders.ACCEPT_CHARSET, "UTF-8");
+
+        HttpEntity<String> request = new HttpEntity<>(JsonUtil.toString(param), headers);
+
+        String result = restTemplate.postForObject(url, request, String.class);
+
+        System.out.println("返回结果" + result);
+    }
+
+    /**
+     * MD5加密
+     *
+     * @param data 待处理数据
+     * @return MD5结果
+     */
+    public static String MD5(String data) throws Exception {
+        java.security.MessageDigest md = MessageDigest.getInstance(SIGN_MD5);
+        byte[] array = md.digest(data.getBytes(StandardCharsets.UTF_8));
+        StringBuilder sb = new StringBuilder();
+        for (byte item : array) {
+            sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3);
+        }
+        return sb.toString();
+    }
+}

+ 363 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfAppletMsgServiceImpl.java

@@ -0,0 +1,363 @@
+package com.zanxiang.game.module.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.zanxiang.erp.security.util.SecurityUtil;
+import com.zanxiang.game.back.base.pojo.enums.OrderStatusEnum;
+import com.zanxiang.game.module.manage.constant.RedisKeyConstant;
+import com.zanxiang.game.module.manage.enums.ExpireTimeEnum;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgOwnerEnum;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgTypeEnum;
+import com.zanxiang.game.module.manage.enums.KfWebSocketMsgEnum;
+import com.zanxiang.game.module.manage.pojo.dto.KfAppletMsgDTO;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.game.module.manage.pojo.dto.PayApplicationDTO;
+import com.zanxiang.game.module.manage.service.*;
+import com.zanxiang.game.module.manage.service.api.KfWxApiService;
+import com.zanxiang.game.module.manage.utils.FileUtil;
+import com.zanxiang.game.module.manage.utils.RedisUtil;
+import com.zanxiang.game.module.mybatis.entity.*;
+import com.zanxiang.module.oss.service.IOssService;
+import com.zanxiang.module.util.JsonUtil;
+import com.zanxiang.module.util.bean.BeanUtil;
+import com.zanxiang.module.util.exception.BaseException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.logging.log4j.util.Strings;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.*;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-28
+ * @description : 小程序客服消息处理
+ */
+@Slf4j
+@Service
+public class KfAppletMsgServiceImpl implements IKfAppletMsgService {
+
+    @Value("${media.realm-name}")
+    private String realmName;
+
+    @Value("${payConfig.wxPay.customH5Url}")
+    private String customH5Url;
+
+    @Autowired
+    private KfWxApiService kfWxApiService;
+
+    @Autowired
+    private IOrderService orderService;
+
+    @Autowired
+    private IGamePayWayService gamePayWayService;
+
+    @Autowired
+    private IPayApplicationService payApplicationService;
+
+    @Autowired
+    private IOssService ossService;
+
+    @Autowired
+    private RedisTemplate<String, String> redisTemplate;
+
+    @Autowired
+    private IKfSessionUserService kfSessionUserService;
+
+    @Autowired
+    private IKfRoomService kfRoomService;
+
+    @Autowired
+    private IGameAppletService gameAppletService;
+
+    @Autowired
+    private IUserService userService;
+
+    @Autowired
+    private IGameUserRoleService gameUserRoleService;
+
+    @Autowired
+    private IKfRoomMsgService kfRoomMsgService;
+
+    @Autowired
+    private RedisUtil<String> redisUtil;
+
+    @Override
+    public void appletMsg(String postData) {
+        log.error("接收到SDK转发的小程序消息, postData : {}", postData);
+        KfAppletMsgDTO kfAppletMsgDTO = JsonUtil.toObj(postData, KfAppletMsgDTO.class);
+        GameApplet gameApplet = gameAppletService.getOne(new LambdaQueryWrapper<GameApplet>()
+                .eq(GameApplet::getGhId, kfAppletMsgDTO.getToUserName()));
+        //小游戏信息不存在, 消息不处理
+        if (gameApplet == null) {
+            return;
+        }
+        //事件消息
+        if (Objects.equals(kfAppletMsgDTO.getMsgType(), KfAppletMsgDTO.MSG_TYPE_EVENT)) {
+            this.eventMsgHandle(gameApplet, kfAppletMsgDTO);
+            return;
+        }
+        //非玩家消息, 不做处理
+        if (kfAppletMsgDTO.getMsgId() == null) {
+            return;
+        }
+        //查询玩家的房间连接状态
+        KfRoom kfRoom = kfRoomService.getOne(new LambdaQueryWrapper<KfRoom>()
+                .eq(KfRoom::getOpenId, kfAppletMsgDTO.getFromUserName())
+                .eq(KfRoom::getGameId, gameApplet.getGameId())
+                .eq(KfRoom::getOnline, Boolean.TRUE));
+        //构造房间消息
+        KfWebSocketMsgDTO.MsgContentBean msgContent = this.getMsgContent(kfAppletMsgDTO);
+        KfRoomMsg kfRoomMsg = this.transform(kfAppletMsgDTO, gameApplet, kfRoom, postData, msgContent);
+        //判断是否请求支付链接
+        String orderId = redisUtil.getCache(RedisKeyConstant.GAME_CUSTOM_PAY + kfAppletMsgDTO.getFromUserName());
+        if (Strings.isNotBlank(orderId) && Objects.equals(kfAppletMsgDTO.getContent(), KfAppletMsgDTO.MSG_CONTENT_PAY)) {
+            Order order = orderService.getOne(new LambdaQueryWrapper<Order>()
+                    .eq(Order::getOrderId, orderId)
+                    .and(qw -> qw.eq(Order::getStatus, OrderStatusEnum.READY_PAY.getValue())
+                            .or().eq(Order::getStatus, OrderStatusEnum.WAIT_PAY.getValue())
+                    )
+            );
+            if (order != null) {
+                this.sendCustomPayMessage(gameApplet.getGameId(), kfAppletMsgDTO.getFromUserName(), kfRoom, order);
+                kfRoomMsg.setReadStatus(Boolean.TRUE);
+                kfRoomMsgService.save(kfRoomMsg);
+                return;
+            }
+        }
+        //保存房间消息
+        kfRoomMsgService.save(kfRoomMsg);
+        //玩家状态更新为待接入状态
+        if (kfRoom == null) {
+            kfSessionUserService.update(new LambdaUpdateWrapper<KfSessionUser>()
+                    .set(KfSessionUser::getIsWait, Boolean.TRUE)
+                    .set(KfSessionUser::getWaitStartTime, LocalDateTime.now())
+                    .set(KfSessionUser::getUpdateTime, LocalDateTime.now())
+                    .eq(KfSessionUser::getOpenId, kfAppletMsgDTO.getFromUserName()));
+        }
+        //消息转发到redis频道
+        this.pushMessage(this.transform(kfRoom, gameApplet.getGameId(), kfRoomMsg, msgContent));
+    }
+
+    private void eventMsgHandle(GameApplet gameApplet, KfAppletMsgDTO kfAppletMsgDTO) {
+        //事件类型
+        String event = kfAppletMsgDTO.getEvent();
+        //非玩家打开会话消息, 不处理
+        if (!Objects.equals(event, KfAppletMsgDTO.EVENT_USER_ENTER_TEMP_SESSION)) {
+            return;
+        }
+        //保存玩家信息
+        this.kfSessionUserUpdateSave(kfAppletMsgDTO, gameApplet);
+        //判断玩家是否存在客服支付订单
+        String customPaySign = RedisKeyConstant.GAME_CUSTOM_PAY_SIGN + kfAppletMsgDTO.getFromUserName();
+        String orderId = redisUtil.getCache(customPaySign);
+        if (Strings.isBlank(orderId)) {
+            return;
+        }
+        //判断玩家当天是否发送过消息
+        if (kfRoomMsgService.count(new LambdaQueryWrapper<KfRoomMsg>()
+                .eq(KfRoomMsg::getOpenId, kfAppletMsgDTO.getFromUserName())
+                .ge(KfRoomMsg::getCreateTime, LocalDateTime.now().with(LocalTime.MIDNIGHT))
+        ) <= 0) {
+            return;
+        }
+        //查询订单
+        Order order = orderService.getOne(new LambdaQueryWrapper<Order>()
+                .eq(Order::getOrderId, orderId)
+                .and(qw -> qw.eq(Order::getStatus, OrderStatusEnum.READY_PAY.getValue())
+                        .or().eq(Order::getStatus, OrderStatusEnum.WAIT_PAY.getValue())
+                )
+        );
+        if (order == null) {
+            return;
+        }
+        this.sendCustomPayMessage(gameApplet.getGameId(), kfAppletMsgDTO.getFromUserName(), null, order);
+        redisUtil.deleteCache(customPaySign);
+        redisUtil.setCache(RedisKeyConstant.GAME_CUSTOM_PAY + kfAppletMsgDTO.getFromUserName(),
+                orderId, ExpireTimeEnum.FIVE_MIN.getTime());
+    }
+
+    private KfWebSocketMsgDTO transform(KfRoom kfRoom, Long gameId, KfRoomMsg kfRoomMsg, KfWebSocketMsgDTO.MsgContentBean msgContent) {
+        //消息类型
+        KfWebSocketMsgEnum kfWebSocketMsgEnum = kfRoom == null ? KfWebSocketMsgEnum.WEBSOCKET_MSG_WAIT_LIST
+                : KfWebSocketMsgEnum.WEBSOCKET_MSG_ROOM_MSG;
+        KfWebSocketMsgDTO kfWebSocketMsgDTO = KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(kfWebSocketMsgEnum)
+                .kfUserId(kfRoom == null ? null : kfRoom.getKfUserId())
+                .gameId(gameId)
+                .roomId(kfRoom == null ? null : kfRoom.getId())
+                .build();
+        //游戏列表
+        kfWebSocketMsgDTO.setGameList(kfRoomService.getKfGameByGameId(gameId));
+        //待接入消息
+        if (Objects.equals(kfWebSocketMsgEnum, KfWebSocketMsgEnum.WEBSOCKET_MSG_WAIT_LIST)) {
+            List<KfWebSocketMsgDTO.WaitUserBean> waitUserList = kfSessionUserService.getWaitUserList(gameId);
+            kfWebSocketMsgDTO.setWaitUserList(waitUserList);
+        }
+        //房间消息
+        if (Objects.equals(kfWebSocketMsgEnum, KfWebSocketMsgEnum.WEBSOCKET_MSG_ROOM_MSG)) {
+            //消息对象
+            KfWebSocketMsgDTO.RoomMsgBean roomMsgBean = BeanUtil.copy(kfRoomMsg, KfWebSocketMsgDTO.RoomMsgBean.class);
+            roomMsgBean.setContent(msgContent);
+            kfWebSocketMsgDTO.setRoomMsgList(Collections.singletonList(roomMsgBean));
+            //房间, 空指针警告只是逻辑警告, kfRoom为空不会走到这里, 走上面的 if 条件
+            kfWebSocketMsgDTO.setRoomList(kfRoomService.getRoomByRoomId(kfRoom.getId()));
+        }
+        return kfWebSocketMsgDTO;
+    }
+
+    private void kfSessionUserUpdateSave(KfAppletMsgDTO kfAppletMsgDTO, GameApplet gameApplet) {
+        KfSessionUser kfSessionUser = kfSessionUserService.getById(kfAppletMsgDTO.getFromUserName());
+        //查询用户
+        User user = userService.getOne(new LambdaQueryWrapper<User>().eq(User::getOpenId, kfAppletMsgDTO.getFromUserName()));
+        //最近角色信息
+        GameUserRole gameUserRole = null;
+        if (user != null) {
+            gameUserRole = gameUserRoleService.getLastGameUserRoleName(user.getId(), user.getGameId());
+        }
+        //不存在玩家信息, 创建保存
+        if (kfSessionUser == null) {
+            kfSessionUserService.save(this.transform(kfAppletMsgDTO, gameApplet, user, gameUserRole));
+            return;
+        }
+        //存在, 更新玩家信息
+        kfSessionUserService.update(new LambdaUpdateWrapper<KfSessionUser>()
+                .set(KfSessionUser::getUserId, user == null ? null : user.getId())
+                .set(KfSessionUser::getLastRoleId, gameUserRole == null ? "0" : gameUserRole.getRoleId())
+                .set(KfSessionUser::getLastRoleName, gameUserRole == null ? kfAppletMsgDTO.getFromUserName() : gameUserRole.getRoleName())
+                .set(gameUserRole != null, KfSessionUser::getServerId, gameUserRole == null ? null : gameUserRole.getServerId())
+                .set(gameUserRole != null, KfSessionUser::getServerName, gameUserRole == null ? null : gameUserRole.getServerName())
+                .set(KfSessionUser::getUpdateTime, LocalDateTime.now())
+                .eq(KfSessionUser::getOpenId, kfAppletMsgDTO.getFromUserName()));
+    }
+
+    private KfSessionUser transform(KfAppletMsgDTO kfAppletMsgDTO, GameApplet gameApplet, User user, GameUserRole gameUserRole) {
+        return KfSessionUser.builder()
+                .openId(kfAppletMsgDTO.getFromUserName())
+                .gameId(gameApplet.getGameId())
+                .userId(user == null ? null : user.getId())
+                .isWait(Boolean.FALSE)
+                .lastRoleId(gameUserRole == null ? "0" : gameUserRole.getRoleId())
+                .lastRoleName(gameUserRole == null ? kfAppletMsgDTO.getFromUserName() : gameUserRole.getRoleName())
+                .serverId(gameUserRole == null ? null : gameUserRole.getServerId())
+                .serverName(gameUserRole == null ? null : gameUserRole.getServerName())
+                .createTime(LocalDateTime.now())
+                .updateTime(LocalDateTime.now())
+                .build();
+    }
+
+    private KfRoomMsg transform(KfAppletMsgDTO kfAppletMsgDTO, GameApplet gameApplet, KfRoom kfRoom, String postData,
+                                KfWebSocketMsgDTO.MsgContentBean msgContent) {
+        return KfRoomMsg.builder()
+                .msgId(String.valueOf(kfAppletMsgDTO.getMsgId()))
+                .msgType(kfAppletMsgDTO.getMsgType())
+                .gameId(gameApplet.getGameId())
+                .openId(kfAppletMsgDTO.getFromUserName())
+                .readStatus(Boolean.FALSE)
+                .roomId(kfRoom == null ? null : kfRoom.getId())
+                .kfUserId(kfRoom == null ? null : kfRoom.getKfUserId())
+                .msgOwner(KfRoomMsgOwnerEnum.KF_MSG_OWNER_USER.getValue())
+                .content(JsonUtil.toString(msgContent))
+                .source(postData)
+                .createTime(LocalDateTime.now())
+                .updateTime(LocalDateTime.now())
+                .build();
+    }
+
+    private KfWebSocketMsgDTO.MsgContentBean getMsgContent(KfAppletMsgDTO kfAppletMsgDTO) {
+        KfWebSocketMsgDTO.MsgContentBean msgContentBean = new KfWebSocketMsgDTO.MsgContentBean();
+        if (Objects.equals(KfRoomMsgTypeEnum.KF_MSG_TYPE_TEXT.getValue(), kfAppletMsgDTO.getMsgType())) {
+            msgContentBean.setText(kfAppletMsgDTO.getContent());
+        }
+        if (Objects.equals(KfRoomMsgTypeEnum.KF_MSG_TYPE_IMAGE.getValue(), kfAppletMsgDTO.getMsgType())) {
+            msgContentBean.setImage(this.mediaConvertOss(kfAppletMsgDTO.getPicUrl()));
+        }
+        return msgContentBean;
+    }
+
+    private String mediaConvertOss(String mediaUrl) {
+        //资源转换
+        MultipartFile multipartFile = FileUtil.urlToMultipartFile(mediaUrl);
+        //生成唯一文件名
+        String fileName = Long.toString(System.currentTimeMillis(), 36) + SecurityUtil.getUserId() + ".jpg";
+        try {
+            ossService.upload("image/" + fileName, multipartFile.getInputStream());
+        } catch (IOException e) {
+            log.error("文件上传oss异常, mediaUrl : {}, e : {}", mediaUrl, e.getMessage());
+            throw new BaseException("文件上传oss异常");
+        }
+        //oss资源地址
+        return this.realmName + "image/" + fileName;
+    }
+
+    private void sendCustomPayMessage(Long gameId, String openId, KfRoom kfRoom, Order order) {
+        //查询订单支付方式
+        GamePayWay gamePayWay = gamePayWayService.getById(order.getGamePayWayId());
+        //查询支付应用信息
+        PayApplicationDTO payApplicationDTO = payApplicationService.getPayApplicationByPayBoxId(gamePayWay.getPayBoxId());
+        //客服支付链接显示图片地址
+        String thumbUrl = gamePayWay.getThumbUrl();
+        //支付配置参数判断
+        if (Strings.isBlank(thumbUrl)) {
+            log.error("客服消息卡片图片地址不存在, gamePayWayDTO : {}", JsonUtil.toString(gamePayWay));
+            return;
+        }
+        //订单金额
+        BigDecimal amount = order.getAmount();
+        //构造跳转链接url
+        URI url = UriComponentsBuilder.fromHttpUrl(this.customH5Url)
+                .queryParam("appId", payApplicationDTO.getAppId())
+                .queryParam("orderId", order.getOrderId())
+                .queryParam("amount", amount)
+                .queryParam("description", "购买" + amount + "元档充值")
+                .build().toUri();
+        //link参数构造
+        Map<String, Object> linkMap = new HashMap<>(4);
+        linkMap.put("title", "点我充值");
+        linkMap.put("description", "点我充值" + amount + "元,用于购买" + amount + "元档充值");
+        linkMap.put("url", url);
+        linkMap.put("thumb_url", thumbUrl);
+        //发送消息
+        Map<String, Object> msgParamMap = new HashMap<>(3);
+        msgParamMap.put("touser", openId);
+        msgParamMap.put("msgtype", KfRoomMsgTypeEnum.KF_MSG_TYPE_LINK.getValue());
+        msgParamMap.put(KfRoomMsgTypeEnum.KF_MSG_TYPE_LINK.getValue(), linkMap);
+        kfWxApiService.sendCustomMessageApi(gameId, msgParamMap);
+        //返回发送的消息内容
+        log.error("客服支付, 发送支付信息 : {}", JsonUtil.toString(msgParamMap));
+        kfRoomMsgService.save(this.transform(openId, gameId, kfRoom, JsonUtil.toString(msgParamMap)));
+    }
+
+    private KfRoomMsg transform(String openId, Long gameId, KfRoom kfRoom, String msgContent) {
+        return KfRoomMsg.builder()
+                .msgId(UUID.randomUUID().toString().replace("-", ""))
+                .msgType(KfRoomMsgTypeEnum.KF_MSG_TYPE_LINK.getValue())
+                .gameId(gameId)
+                .openId(openId)
+                .readStatus(kfRoom != null)
+                .roomId(kfRoom == null ? null : kfRoom.getId())
+                .kfUserId(kfRoom == null ? null : kfRoom.getKfUserId())
+                .msgOwner(KfRoomMsgOwnerEnum.KF_MSG_OWNER_SYSTEM.getValue())
+                .content(msgContent)
+                .source(msgContent)
+                .createTime(LocalDateTime.now())
+                .updateTime(LocalDateTime.now())
+                .build();
+    }
+
+    /**
+     * 消息发送到redis广播
+     */
+    private void pushMessage(KfWebSocketMsgDTO kfWebSocketMsgDTO) {
+        redisTemplate.convertAndSend(RedisKeyConstant.KF_MSG_REDIS_LISTEN_TOPIC, JsonUtil.toString(kfWebSocketMsgDTO));
+    }
+}

+ 82 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfQuickReplyServiceImpl.java

@@ -0,0 +1,82 @@
+package com.zanxiang.game.module.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zanxiang.erp.security.util.SecurityUtil;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.game.module.manage.pojo.params.KfWebSocketMsgParam;
+import com.zanxiang.game.module.manage.service.IKfQuickReplyService;
+import com.zanxiang.game.module.mybatis.entity.KfQuickReply;
+import com.zanxiang.game.module.mybatis.mapper.KfQuickReplyMapper;
+import com.zanxiang.module.util.bean.BeanUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-03-05
+ * @description : 客服快捷回复
+ */
+@Slf4j
+@Service
+public class KfQuickReplyServiceImpl extends ServiceImpl<KfQuickReplyMapper, KfQuickReply> implements IKfQuickReplyService {
+
+    @Override
+    public boolean quickReplyAdd(KfWebSocketMsgParam.QuickReplyBean param) {
+        return super.save(KfQuickReply.builder()
+                .kfUserId(SecurityUtil.getUserId())
+                .content(param.getContent())
+                .status(Boolean.TRUE)
+                .createTime(LocalDateTime.now())
+                .updateTime(LocalDateTime.now())
+                .build());
+    }
+
+    @Override
+    public boolean quickReplyDelete(KfWebSocketMsgParam.QuickReplyBean param) {
+        return super.update(new LambdaUpdateWrapper<KfQuickReply>()
+                .set(KfQuickReply::getStatus, Boolean.FALSE)
+                .set(KfQuickReply::getUpdateTime, LocalDateTime.now())
+                .eq(KfQuickReply::getId, param.getId())
+                .eq(KfQuickReply::getKfUserId, SecurityUtil.getUserId())
+        );
+    }
+
+    @Override
+    public boolean quickReplyUpdate(KfWebSocketMsgParam.QuickReplyBean param) {
+        return super.update(new LambdaUpdateWrapper<KfQuickReply>()
+                .set(KfQuickReply::getContent, param.getContent())
+                .set(KfQuickReply::getUpdateTime, LocalDateTime.now())
+                .eq(KfQuickReply::getId, param.getId())
+                .eq(KfQuickReply::getKfUserId, SecurityUtil.getUserId())
+        );
+    }
+
+    @Override
+    public Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.QuickReplyBean>> quickReplyList(KfWebSocketMsgParam.PageBean pageBean) {
+        Page<KfQuickReply> kfQuickReplyPage = super.page(new Page<>(pageBean.getPageNum(), pageBean.getPageSize()),
+                new QueryWrapper<KfQuickReply>().lambda()
+                        .eq(KfQuickReply::getKfUserId, SecurityUtil.getUserId())
+                        .eq(KfQuickReply::getStatus, Boolean.TRUE)
+                        .orderByDesc(KfQuickReply::getCreateTime)
+        );
+        List<KfWebSocketMsgDTO.QuickReplyBean> quickReplyList = kfQuickReplyPage.getRecords().stream()
+                .map(this::transform).collect(Collectors.toList());
+        return Tuples.of(KfWebSocketMsgDTO.transformPage(kfQuickReplyPage), quickReplyList);
+    }
+
+    private KfWebSocketMsgDTO.QuickReplyBean transform(KfQuickReply kfQuickReply) {
+        if (kfQuickReply == null) {
+            return null;
+        }
+        return BeanUtil.copy(kfQuickReply, KfWebSocketMsgDTO.QuickReplyBean.class);
+    }
+}

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

@@ -0,0 +1,91 @@
+package com.zanxiang.game.module.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgOwnerEnum;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgTypeEnum;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.game.module.manage.pojo.params.KfWebSocketMsgParam;
+import com.zanxiang.game.module.manage.service.IKfRoomMsgService;
+import com.zanxiang.game.module.mybatis.entity.KfRoom;
+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.bean.BeanUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.stereotype.Service;
+import reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天房间消息
+ */
+@Slf4j
+@Service
+public class KfRoomMsgServiceImpl extends ServiceImpl<KfRoomMsgMapper, KfRoomMsg> implements IKfRoomMsgService {
+
+    @Override
+    public Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.RoomMsgBean>> msgRoomHistory(Long roomId, KfWebSocketMsgParam.PageBean pageBean) {
+        Page<KfRoomMsg> kfRoomMsgPage = super.page(new Page<>(pageBean.getPageNum(), pageBean.getPageSize()),
+                new QueryWrapper<KfRoomMsg>().lambda()
+                        .eq(KfRoomMsg::getRoomId, roomId)
+                        .orderByDesc(KfRoomMsg::getCreateTime)
+        );
+        //构造消息列表
+        List<KfWebSocketMsgDTO.RoomMsgBean> roomMsgBeanList = kfRoomMsgPage.getRecords().stream()
+                .map(this::transform).collect(Collectors.toList());
+        //更新消息已读状态
+        List<String> msgIdList = roomMsgBeanList.stream()
+                .filter(msg -> Objects.equals(msg.getReadStatus(), Boolean.FALSE))
+                .map(KfWebSocketMsgDTO.RoomMsgBean::getMsgId)
+                .collect(Collectors.toList());
+        if (CollectionUtils.isNotEmpty(msgIdList)) {
+            super.update(new LambdaUpdateWrapper<KfRoomMsg>()
+                    .set(KfRoomMsg::getReadStatus, Boolean.TRUE)
+                    .set(KfRoomMsg::getUpdateTime, LocalDateTime.now())
+                    .in(KfRoomMsg::getMsgId, msgIdList));
+        }
+        //返回分页数据, 消息列表
+        return Tuples.of(KfWebSocketMsgDTO.transformPage(kfRoomMsgPage), roomMsgBeanList);
+    }
+
+    private KfWebSocketMsgDTO.RoomMsgBean transform(KfRoomMsg kfRoomMsg) {
+        if (kfRoomMsg == null) {
+            return null;
+        }
+        KfWebSocketMsgDTO.RoomMsgBean roomMsgBean = BeanUtil.copy(kfRoomMsg, KfWebSocketMsgDTO.RoomMsgBean.class);
+        roomMsgBean.setMsgType(KfRoomMsgTypeEnum.getMsgTypeEnum(kfRoomMsg.getMsgType()));
+        roomMsgBean.setContent(JsonUtil.toObj(kfRoomMsg.getContent(), KfWebSocketMsgDTO.MsgContentBean.class));
+        return roomMsgBean;
+    }
+
+    @Override
+    public boolean sendMsgSave(Long gameId, KfRoom kfRoom, KfWebSocketMsgParam.MsgContentBean msgContent) {
+        return super.save(KfRoomMsg.builder()
+                .msgId(UUID.randomUUID().toString().replace("-", ""))
+                .msgType(msgContent.getMsgType().getValue())
+                .gameId(gameId)
+                .openId(kfRoom.getOpenId())
+                .userId(kfRoom.getUserId())
+                .kfUserId(kfRoom.getKfUserId())
+                .readStatus(Boolean.TRUE)
+                .roomId(kfRoom.getId())
+                .msgOwner(KfRoomMsgOwnerEnum.KF_MSG_OWNER_KF.getValue())
+                .content(JsonUtil.toString(BeanUtil.copy(msgContent, KfWebSocketMsgDTO.MsgContentBean.class)))
+                .source(JsonUtil.toString(msgContent))
+                .createTime(LocalDateTime.now())
+                .updateTime(LocalDateTime.now())
+                .build());
+    }
+}

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

@@ -0,0 +1,285 @@
+package com.zanxiang.game.module.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zanxiang.erp.security.util.SecurityUtil;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgOwnerEnum;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgTypeEnum;
+import com.zanxiang.game.module.manage.enums.OrderStateEnum;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.game.module.manage.pojo.params.KfWebSocketMsgParam;
+import com.zanxiang.game.module.manage.service.*;
+import com.zanxiang.game.module.mybatis.entity.*;
+import com.zanxiang.game.module.mybatis.mapper.KfRoomMapper;
+import com.zanxiang.module.util.JsonUtil;
+import com.zanxiang.module.util.bean.BeanUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import reactor.util.function.Tuple2;
+import reactor.util.function.Tuples;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天房间
+ */
+@Slf4j
+@Service
+public class KfRoomServiceImpl extends ServiceImpl<KfRoomMapper, KfRoom> implements IKfRoomService {
+
+    @Autowired
+    private IGameAuthService gameAuthService;
+
+    @Autowired
+    private IGameAppletService gameAppletService;
+
+    @Autowired
+    private IKfRoomMsgService kfRoomMsgService;
+
+    @Autowired
+    private IUserService userService;
+
+    @Autowired
+    private IOrderService orderService;
+
+    @Autowired
+    private IKfSessionUserService kfSessionUserService;
+
+    @Autowired
+    private IGameUserRoleService gameUserRoleService;
+
+    @Override
+    public Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.RoomBean>> getFinishRoomList(Long gameId, KfWebSocketMsgParam.PageBean pageBean) {
+        Page<KfRoom> kfRoomPage = super.page(new Page<>(pageBean.getPageNum(), pageBean.getPageSize()),
+                new QueryWrapper<KfRoom>().lambda()
+                        .eq(KfRoom::getGameId, gameId)
+                        .eq(KfRoom::getOnline, Boolean.FALSE)
+                        .orderByDesc(KfRoom::getUpdateTime)
+        );
+        List<KfWebSocketMsgDTO.RoomBean> roomBeanList = kfRoomPage.getRecords().stream()
+                .map(this::transform).collect(Collectors.toList());
+        return Tuples.of(KfWebSocketMsgDTO.transformPage(kfRoomPage), roomBeanList);
+    }
+
+    @Override
+    public Long userJoinRoom(String openId, Long gameId) {
+        KfRoom kfRoom = super.getOne(new LambdaQueryWrapper<KfRoom>()
+                .eq(KfRoom::getOpenId, openId)
+                .eq(KfRoom::getKfUserId, SecurityUtil.getUserId()));
+        if (kfRoom == null) {
+            kfRoom = this.transform(openId, gameId);
+            super.save(kfRoom);
+            return kfRoom.getId();
+        }
+        super.update(new LambdaUpdateWrapper<KfRoom>()
+                .set(KfRoom::getOnline, Boolean.TRUE)
+                .set(KfRoom::getUpdateTime, LocalDateTime.now())
+                .eq(KfRoom::getId, kfRoom.getId())
+        );
+        return kfRoom.getId();
+    }
+
+    private KfRoom transform(String openId, Long gameId) {
+        User user = userService.getOne(new LambdaQueryWrapper<User>()
+                .eq(User::getGameId, gameId)
+                .eq(User::getOpenId, openId));
+        return KfRoom.builder()
+                .gameId(gameId)
+                .openId(openId)
+                .userId(user == null ? null : user.getId())
+                .kfUserId(SecurityUtil.getUserId())
+                .online(Boolean.TRUE)
+                .createTime(LocalDateTime.now())
+                .updateTime(LocalDateTime.now())
+                .build();
+    }
+
+    @Override
+    public List<KfWebSocketMsgDTO.GameBean> getKfGameList() {
+        List<GameAuth> gameAuthList = gameAuthService.list(new LambdaQueryWrapper<GameAuth>()
+                .eq(!SecurityUtil.isAdmin(), GameAuth::getUserId, SecurityUtil.getUserId()));
+        if (!SecurityUtil.isAdmin() && CollectionUtils.isEmpty(gameAuthList)) {
+            return Collections.emptyList();
+        }
+        return gameAppletService.list(new LambdaQueryWrapper<GameApplet>()
+                .in(!SecurityUtil.isAdmin(), GameApplet::getGameId,
+                        gameAuthList.stream().map(GameAuth::getGameId).collect(Collectors.toSet()))
+                .eq(GameApplet::getType, 1)
+        ).stream().map(this::transform).collect(Collectors.toList());
+    }
+
+    @Override
+    public List<KfWebSocketMsgDTO.GameBean> getKfGameByGameId(Long gameId) {
+        GameApplet gameApplet = gameAppletService.getOne(new LambdaQueryWrapper<GameApplet>()
+                .eq(GameApplet::getGameId, gameId));
+        if (gameApplet == null) {
+            return Collections.emptyList();
+        }
+        return Collections.singletonList(this.transform(gameApplet));
+    }
+
+    private KfWebSocketMsgDTO.GameBean transform(GameApplet gameApplet) {
+        if (gameApplet == null) {
+            return null;
+        }
+        int unReadMsgCount = kfRoomMsgService.count(new LambdaQueryWrapper<KfRoomMsg>()
+                .eq(KfRoomMsg::getGameId, gameApplet.getGameId())
+                .eq(KfRoomMsg::getReadStatus, Boolean.FALSE)
+                .ne(KfRoomMsg::getMsgOwner, KfRoomMsgOwnerEnum.KF_MSG_OWNER_SYSTEM.getValue()));
+        KfWebSocketMsgDTO.GameBean gameBean = BeanUtil.copy(gameApplet, KfWebSocketMsgDTO.GameBean.class);
+        gameBean.setUnReadMsgCount(unReadMsgCount);
+        return gameBean;
+    }
+
+    @Override
+    public List<KfWebSocketMsgDTO.RoomBean> getOnlineRoomList(Long gameId) {
+        return super.list(new LambdaQueryWrapper<KfRoom>()
+                .eq(KfRoom::getKfUserId, SecurityUtil.getUserId())
+                .eq(KfRoom::getGameId, gameId)
+                .eq(KfRoom::getOnline, Boolean.TRUE)
+        ).stream().map(this::transform).collect(Collectors.toList());
+    }
+
+    @Override
+    public List<KfWebSocketMsgDTO.RoomBean> getRoomByRoomId(Long roomId) {
+        KfRoom kfRoom = super.getById(roomId);
+        if (kfRoom == null) {
+            return Collections.emptyList();
+        }
+        return Collections.singletonList(this.transform(kfRoom));
+    }
+
+    private KfWebSocketMsgDTO.RoomBean transform(KfRoom kfRoom) {
+        if (kfRoom == null) {
+            return null;
+        }
+        KfWebSocketMsgDTO.RoomBean roomBean = BeanUtil.copy(kfRoom, KfWebSocketMsgDTO.RoomBean.class);
+        roomBean.setRoomId(kfRoom.getId());
+        //最近角色信息
+        KfSessionUser kfSessionUser = kfSessionUserService.getById(kfRoom.getOpenId());
+        if (kfSessionUser != null) {
+            roomBean.setLastRoleId(kfSessionUser.getLastRoleId());
+            roomBean.setLastRoleName(kfSessionUser.getLastRoleName());
+        }
+        //未读消息数量
+        int unReadMsgCount = kfRoomMsgService.count(new LambdaQueryWrapper<KfRoomMsg>()
+                .eq(KfRoomMsg::getRoomId, kfRoom.getId())
+                .eq(KfRoomMsg::getReadStatus, Boolean.FALSE)
+                .ne(KfRoomMsg::getMsgOwner, KfRoomMsgOwnerEnum.KF_MSG_OWNER_SYSTEM.getValue()));
+        roomBean.setUnReadMsgCount(unReadMsgCount);
+        //最后一条消息
+        KfRoomMsg kfRoomMsg = kfRoomMsgService.getOne(new LambdaQueryWrapper<KfRoomMsg>()
+                .eq(KfRoomMsg::getRoomId, kfRoom.getId())
+                .orderByDesc(KfRoomMsg::getCreateTime)
+                .last("limit 1"));
+        if (kfRoomMsg != null) {
+            if (Objects.equals(kfRoomMsg.getMsgType(), KfRoomMsgTypeEnum.KF_MSG_TYPE_TEXT.getValue())) {
+                KfWebSocketMsgDTO.MsgContentBean msgContent = JsonUtil.toObj(kfRoomMsg.getContent(), KfWebSocketMsgDTO.MsgContentBean.class);
+                roomBean.setLastMsg(msgContent.getText());
+            }
+            if (Objects.equals(kfRoomMsg.getMsgType(), KfRoomMsgTypeEnum.KF_MSG_TYPE_IMAGE.getValue())) {
+                roomBean.setLastMsg("[图片]");
+            }
+            if (Objects.equals(kfRoomMsg.getMsgType(), KfRoomMsgTypeEnum.KF_MSG_TYPE_LINK.getValue())) {
+                roomBean.setLastMsg("[充值]");
+            }
+            roomBean.setLastMsgTime(kfRoomMsg.getCreateTime());
+            roomBean.setMsgOwner(kfRoomMsg.getMsgOwner());
+        }
+        return roomBean;
+    }
+
+    @Override
+    public KfWebSocketMsgDTO.UserBean getUserBean(String openId) {
+        KfSessionUser kfSessionUser = kfSessionUserService.getById(openId);
+        KfWebSocketMsgDTO.UserBean userBean = BeanUtil.copy(kfSessionUser, KfWebSocketMsgDTO.UserBean.class);
+        if (userBean == null || userBean.getUserId() == null) {
+            return userBean;
+        }
+        //查询订单
+        List<Order> orderList = orderService.list(new LambdaQueryWrapper<Order>()
+                .select(Order::getOrderId, Order::getAmount, Order::getPayTime)
+                .eq(Order::getUserId, userBean.getUserId())
+                .eq(kfSessionUser.getGameId() != null, Order::getGameId, kfSessionUser.getGameId())
+                .eq(Order::getStatus, OrderStateEnum.SUCCESS_PAY.getCode()));
+        //设置参数
+        if (CollectionUtils.isNotEmpty(orderList)) {
+            //订单数
+            userBean.setOrderCount(orderList.size());
+            //最大订单金额
+            Optional<Order> maxAmountOrder = orderList.stream()
+                    .max(Comparator.comparing(Order::getAmount));
+            userBean.setOrderMaxAmount(maxAmountOrder.map(Order::getAmount).orElse(null));
+            //最近支付时间
+            Optional<Order> lastPayTimeOrder = orderList.stream()
+                    .max(Comparator.comparing(Order::getPayTime));
+            userBean.setLastPayTime(lastPayTimeOrder.map(Order::getPayTime).orElse(null));
+            //支付总金额
+            BigDecimal totalAmount = orderList.stream()
+                    .map(Order::getAmount)
+                    .reduce(BigDecimal.ZERO, BigDecimal::add);
+            userBean.setOrderAmountSum(totalAmount);
+        }
+        return userBean;
+    }
+
+    @Override
+    public Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.GameRoleBean>> getRoleBeanList(String openId, KfWebSocketMsgParam.PageBean pageBean) {
+        KfSessionUser kfSessionUser = kfSessionUserService.getById(openId);
+        if (kfSessionUser == null || kfSessionUser.getUserId() == null) {
+            return Tuples.of(KfWebSocketMsgDTO.defaultPage(pageBean.getPageNum(), pageBean.getPageSize()), Collections.emptyList());
+        }
+        Page<GameUserRole> gameUserRolePage = gameUserRoleService.page(new Page<>(pageBean.getPageNum(), pageBean.getPageSize()),
+                new QueryWrapper<GameUserRole>().lambda()
+                        .eq(GameUserRole::getUserId, kfSessionUser.getUserId())
+                        .orderByDesc(GameUserRole::getCreateTime)
+        );
+        //构造角色列表
+        List<KfWebSocketMsgDTO.GameRoleBean> gameRoleList = gameUserRolePage.getRecords().stream()
+                .map(this::transform).collect(Collectors.toList());
+        return Tuples.of(KfWebSocketMsgDTO.transformPage(gameUserRolePage), gameRoleList);
+    }
+
+    private KfWebSocketMsgDTO.GameRoleBean transform(GameUserRole gameUserRole) {
+        if (gameUserRole == null) {
+            return null;
+        }
+        return BeanUtil.copy(gameUserRole, KfWebSocketMsgDTO.GameRoleBean.class);
+    }
+
+    @Override
+    public Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.OrderBean>> getOrderBeanList(String openId, KfWebSocketMsgParam.PageBean pageBean) {
+        KfSessionUser kfSessionUser = kfSessionUserService.getById(openId);
+        if (kfSessionUser == null || kfSessionUser.getUserId() == null) {
+            return Tuples.of(KfWebSocketMsgDTO.defaultPage(pageBean.getPageNum(), pageBean.getPageSize()), Collections.emptyList());
+        }
+        Page<Order> orderPage = orderService.page(new Page<>(pageBean.getPageNum(), pageBean.getPageSize()),
+                new QueryWrapper<Order>().lambda()
+                        .eq(Order::getUserId, kfSessionUser.getUserId())
+                        .eq(Order::getStatus, OrderStateEnum.SUCCESS_PAY.getCode())
+                        .orderByDesc(Order::getCreateTime)
+        );
+        //构造订单列表
+        List<KfWebSocketMsgDTO.OrderBean> orderList = orderPage.getRecords().stream()
+                .map(this::transform).collect(Collectors.toList());
+        return Tuples.of(KfWebSocketMsgDTO.transformPage(orderPage), orderList);
+    }
+
+    private KfWebSocketMsgDTO.OrderBean transform(Order order) {
+        if (order == null) {
+            return null;
+        }
+        return BeanUtil.copy(order, KfWebSocketMsgDTO.OrderBean.class);
+    }
+
+}

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

@@ -0,0 +1,82 @@
+package com.zanxiang.game.module.manage.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgTypeEnum;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.game.module.manage.service.IKfRoomMsgService;
+import com.zanxiang.game.module.manage.service.IKfSessionUserService;
+import com.zanxiang.game.module.mybatis.entity.KfRoomMsg;
+import com.zanxiang.game.module.mybatis.entity.KfSessionUser;
+import com.zanxiang.game.module.mybatis.mapper.KfSessionUserMapper;
+import com.zanxiang.module.util.JsonUtil;
+import com.zanxiang.module.util.bean.BeanUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服聊天玩家信息
+ */
+@Slf4j
+@Service
+public class KfSessionUserServiceImpl extends ServiceImpl<KfSessionUserMapper, KfSessionUser> implements IKfSessionUserService {
+
+    @Autowired
+    private IKfRoomMsgService kfRoomMsgService;
+
+    @Override
+    public List<KfWebSocketMsgDTO.WaitUserBean> getWaitUserList(Long gameId) {
+        return super.list(new LambdaQueryWrapper<KfSessionUser>()
+                .eq(KfSessionUser::getGameId, gameId)
+                .eq(KfSessionUser::getIsWait, Boolean.TRUE)
+        ).stream().map(this::transform).collect(Collectors.toList());
+    }
+
+    private KfWebSocketMsgDTO.WaitUserBean transform(KfSessionUser kfSessionUser) {
+        if (kfSessionUser == null) {
+            return null;
+        }
+        KfWebSocketMsgDTO.WaitUserBean waitUserBean = BeanUtil.copy(kfSessionUser, KfWebSocketMsgDTO.WaitUserBean.class);
+        Page<KfRoomMsg> kfRoomMsgPage = kfRoomMsgService.page(new Page<>(1, 10),
+                new QueryWrapper<KfRoomMsg>().lambda()
+                        .eq(KfRoomMsg::getOpenId, kfSessionUser.getOpenId())
+                        .eq(KfRoomMsg::getReadStatus, Boolean.FALSE)
+                        .isNull(KfRoomMsg::getRoomId)
+                        .orderByDesc(KfRoomMsg::getCreateTime));
+        List<KfWebSocketMsgDTO.WaitUserMsgBean> waitUserMsgList = kfRoomMsgPage.getRecords()
+                .stream().map(this::transform).collect(Collectors.toList());
+        waitUserBean.setWaitUserMsgList(waitUserMsgList);
+        waitUserBean.setUnReadMsgCount(kfRoomMsgPage.getTotal());
+        return waitUserBean;
+    }
+
+    private KfWebSocketMsgDTO.WaitUserMsgBean transform(KfRoomMsg kfRoomMsg) {
+        if (kfRoomMsg == null) {
+            return null;
+        }
+        //等待消息对象
+        KfWebSocketMsgDTO.WaitUserMsgBean waitUserMsgBean = BeanUtil.copy(kfRoomMsg, KfWebSocketMsgDTO.WaitUserMsgBean.class);
+        //消息内容解析
+        KfWebSocketMsgDTO.MsgContentBean msgContent = JsonUtil.toObj(waitUserMsgBean.getContent(), KfWebSocketMsgDTO.MsgContentBean.class);
+        //消息内容转换
+        if (Objects.equals(waitUserMsgBean.getMsgType(), KfRoomMsgTypeEnum.KF_MSG_TYPE_TEXT.getValue())) {
+            waitUserMsgBean.setContent(msgContent.getText());
+        }
+        if (Objects.equals(waitUserMsgBean.getMsgType(), KfRoomMsgTypeEnum.KF_MSG_TYPE_IMAGE.getValue())) {
+            waitUserMsgBean.setContent("[图片]");
+        }
+        if (Objects.equals(waitUserMsgBean.getMsgType(), KfRoomMsgTypeEnum.KF_MSG_TYPE_LINK.getValue())) {
+            waitUserMsgBean.setContent("[充值]");
+        }
+        return waitUserMsgBean;
+    }
+}

+ 0 - 16
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/OssServiceImpl.java

@@ -1,16 +0,0 @@
-package com.zanxiang.game.module.manage.service.impl;
-
-import com.zanxiang.game.module.manage.service.IOssService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-/**
- * @author : lingfeng
- * @time : 2022-07-12
- * @description : 阿里云oss
- */
-@Slf4j
-@Service
-public class OssServiceImpl implements IOssService {
-
-}

+ 16 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/PayApplicationServiceImpl.java

@@ -14,7 +14,9 @@ import com.zanxiang.game.module.manage.pojo.vo.PayApplicationChoiceVO;
 import com.zanxiang.game.module.manage.pojo.vo.PayApplicationVO;
 import com.zanxiang.game.module.manage.service.IGameAppletService;
 import com.zanxiang.game.module.manage.service.IPayApplicationService;
+import com.zanxiang.game.module.manage.service.IPayBoxService;
 import com.zanxiang.game.module.mybatis.entity.PayApplication;
+import com.zanxiang.game.module.mybatis.entity.PayBox;
 import com.zanxiang.game.module.mybatis.mapper.PayApplicationMapper;
 import com.zanxiang.module.util.bean.BeanUtil;
 import com.zanxiang.module.util.exception.BaseException;
@@ -43,6 +45,9 @@ public class PayApplicationServiceImpl extends ServiceImpl<PayApplicationMapper,
     @Autowired
     private IGameAppletService gameAppletService;
 
+    @Autowired
+    private IPayBoxService payBoxService;
+
     @Override
     public List<PayApplicationChoiceVO> payApplicationChoiceList() {
         List<PayApplication> payApplicationList = super.list(new LambdaQueryWrapper<PayApplication>()
@@ -203,4 +208,15 @@ public class PayApplicationServiceImpl extends ServiceImpl<PayApplicationMapper,
         }
         return BeanUtil.copy(payApplication, PayApplicationDTO.class);
     }
+
+    @Override
+    public PayApplicationDTO getPayApplicationByPayBoxId(Long payBoxId) {
+        PayBox payBox = payBoxService.getById(payBoxId);
+        PayApplication payApplication = super.getOne(new LambdaQueryWrapper<PayApplication>()
+                .eq(PayApplication::getAppId, payBox.getAppId()));
+        if (payApplication == null) {
+            throw new BaseException("参数错误, 支付商城小程序信息不存在");
+        }
+        return BeanUtil.copy(payApplication, PayApplicationDTO.class);
+    }
 }

+ 91 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/utils/FileUtil.java

@@ -0,0 +1,91 @@
+package com.zanxiang.game.module.manage.utils;
+
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.FileItemFactory;
+import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.commons.CommonsMultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+/**
+ * @author : lingfeng
+ * @time : 2023-02-06
+ * @description : 文件工具类
+ */
+public class FileUtil {
+
+    /**
+     * 网络url转MultipartFile
+     *
+     * @param url : 网络资源url
+     * @return {@link MultipartFile}
+     */
+    public static MultipartFile urlToMultipartFile(String url) {
+        byte[] bytes = downloadResources(url);
+        String name = "mediaFile" + url.substring(url.lastIndexOf("."));
+        return getMultipartFile(name, bytes);
+    }
+
+    /**
+     * 下载资源
+     *
+     * @param url url
+     * @return {@link byte[]}
+     */
+    private static byte[] downloadResources(String url) {
+        URL urlConnection;
+        HttpURLConnection connection = null;
+        try {
+            urlConnection = new URL(url);
+            connection = (HttpURLConnection) urlConnection.openConnection();
+            InputStream in = connection.getInputStream();
+            byte[] buffer = new byte[1024];
+            int len;
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            while ((len = in.read(buffer)) != -1) {
+                out.write(buffer, 0, len);
+            }
+            in.close();
+            out.close();
+            return out.toByteArray();
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 得到多部分文件
+     *
+     * @param name  名字
+     * @param bytes 字节
+     * @return {@link MultipartFile}
+     */
+    private static MultipartFile getMultipartFile(String name, byte[] bytes) {
+        MultipartFile multipartFile = null;
+        ByteArrayInputStream in;
+        try {
+            in = new ByteArrayInputStream(bytes);
+            FileItemFactory factory = new DiskFileItemFactory(16, null);
+            FileItem fileItem = factory.createItem("mainFile", "text/plain", false, name);
+            IOUtils.copy(new ByteArrayInputStream(bytes), fileItem.getOutputStream());
+            multipartFile = new CommonsMultipartFile(fileItem);
+            in.close();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return multipartFile;
+    }
+
+
+}

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

@@ -0,0 +1,64 @@
+package com.zanxiang.game.module.manage.websocket;
+
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.module.util.JsonUtil;
+import lombok.extern.slf4j.Slf4j;
+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;
+import java.util.List;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服消息redis监听器
+ */
+@Component
+@Slf4j
+public class KfMsgRedisListener implements MessageListener {
+
+    @Autowired
+    private KfMsgWebSocketSessionRegistry kfMsgWebSocketSessionRegistry;
+
+    @Override
+    public void onMessage(Message message, byte[] pattern) {
+        //从redis中拿到的消息
+        String messageBody = new String(message.getBody());
+        log.error("redis监听器监听到消息 messageBody : {}", messageBody);
+        //转化消息对象
+        KfWebSocketMsgDTO kfWebSocketMsgDTO = JsonUtil.toObj(messageBody, KfWebSocketMsgDTO.class);
+        Long kfUserId = kfWebSocketMsgDTO.getKfUserId();
+        //发送给指定客服
+        if (kfUserId != null) {
+            log.error("发送消息给指定客服 kfUserId : {}, kfWebSocketMsgDTO : {}", kfUserId, JsonUtil.toString(kfWebSocketMsgDTO));
+            WebSocketSession session = kfMsgWebSocketSessionRegistry.getSession(kfUserId);
+            if (session != null && session.isOpen()) {
+                try {
+                    session.sendMessage(new TextMessage(JsonUtil.toString(kfWebSocketMsgDTO)));
+                } catch (Exception e) {
+                    log.error("发送消息给指定客服异常, kfUserId : {}, kfWebSocketMsgDTO : {}, e : {}", kfUserId,
+                            JsonUtil.toString(kfWebSocketMsgDTO), e.getMessage());
+                }
+            }
+            return;
+        }
+        //发送给所有在线客服
+        log.error("发送消息给所有客服 kfWebSocketMsgDTO : {}", JsonUtil.toString(kfWebSocketMsgDTO));
+        List<WebSocketSession> openSessions = kfMsgWebSocketSessionRegistry.getAllSessions();
+        openSessions.forEach(session -> {
+            if (session != null && session.isOpen()) {
+                try {
+                    session.sendMessage(new TextMessage(JsonUtil.toString(kfWebSocketMsgDTO)));
+                } catch (Exception e) {
+                    log.error("发送消息给所有客服异常, kfUserId : {}, kfWebSocketMsgDTO : {}, e : {}",
+                            session.getAttributes().get("kfUserId"), JsonUtil.toString(kfWebSocketMsgDTO), e.getMessage());
+                }
+            }
+        });
+    }
+}

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

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

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

@@ -0,0 +1,493 @@
+package com.zanxiang.game.module.manage.websocket;
+
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.zanxiang.erp.base.pojo.TokenInfo;
+import com.zanxiang.erp.security.util.SecurityUtil;
+import com.zanxiang.game.module.manage.constant.RedisKeyConstant;
+import com.zanxiang.game.module.manage.enums.KfOperateEnum;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgOwnerEnum;
+import com.zanxiang.game.module.manage.enums.KfRoomMsgTypeEnum;
+import com.zanxiang.game.module.manage.enums.KfWebSocketMsgEnum;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.game.module.manage.pojo.params.KfWebSocketMsgParam;
+import com.zanxiang.game.module.manage.service.IKfQuickReplyService;
+import com.zanxiang.game.module.manage.service.IKfRoomMsgService;
+import com.zanxiang.game.module.manage.service.IKfRoomService;
+import com.zanxiang.game.module.manage.service.IKfSessionUserService;
+import com.zanxiang.game.module.manage.service.api.KfWxApiService;
+import com.zanxiang.game.module.manage.utils.FileUtil;
+import com.zanxiang.game.module.mybatis.entity.KfRoom;
+import com.zanxiang.game.module.mybatis.entity.KfRoomMsg;
+import com.zanxiang.game.module.mybatis.entity.KfSessionUser;
+import com.zanxiang.module.redis.service.IDistributedLockComponent;
+import com.zanxiang.module.util.JsonUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.logging.log4j.util.Strings;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.*;
+import reactor.util.function.Tuple2;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-02-23
+ * @description : 客服消息Websocket处理器
+ */
+@Slf4j
+@Lazy(value = false)
+@Component
+public class KfMsgWebsocketHandler implements WebSocketHandler {
+
+    @Autowired
+    private KfMsgWebSocketSessionRegistry kfMsgWebSocketSessionRegistry;
+
+    @Autowired
+    private IKfRoomService kfRoomService;
+
+    @Autowired
+    private IKfSessionUserService kfSessionUserService;
+
+    @Autowired
+    private IKfRoomMsgService kfRoomMsgService;
+
+    @Autowired
+    private IDistributedLockComponent distributedLockComponent;
+
+    @Autowired
+    private KfWxApiService wxApiService;
+
+    @Autowired
+    private IKfQuickReplyService kfQuickReplyService;
+
+    /**
+     * websocket连接建立成功
+     */
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+    }
+
+    /**
+     * 收到消息
+     */
+    @Override
+    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
+        String msgStr = message.getPayload().toString();
+        //消息解析
+        KfWebSocketMsgParam param = JsonUtil.toObj(msgStr, KfWebSocketMsgParam.class);
+        //消息类型
+        KfWebSocketMsgEnum webSocketMsgType = param.getWebSocketMsgType();
+        //游戏id
+        Long gameId = param.getGameId();
+        //参数验证不通过, 直接结束
+        if (!this.paramCheck(session, webSocketMsgType, param.getToken(), gameId)) {
+            return;
+        }
+        //处理不同类型的消息
+        switch (webSocketMsgType) {
+            case WEBSOCKET_MSG_KF_HAND_SHAKE:
+                kfHandShake(session, webSocketMsgType);
+                break;
+            case WEBSOCKET_MSG_KF_CREATE_CONNECT:
+                kfCreateConnect(session, webSocketMsgType, gameId);
+                break;
+            case WEBSOCKET_MSG_USER_CONNECT_JOIN:
+                userConnectJoin(session, param);
+                break;
+            case WEBSOCKET_MSG_ROOM_HISTORY:
+                msgRoomHistory(session, param);
+                break;
+            case WEBSOCKET_MSG_KF_SEND:
+                kfSendMsg(session, param);
+                break;
+            case WEBSOCKET_MSG_FINISH_ROOM_LIST:
+                finishRoomList(session, param);
+                break;
+            case WEBSOCKET_MSG_FINISH_SESSION:
+                kfFinishSession(session, webSocketMsgType, gameId, param.getRoomId());
+                break;
+            case WEBSOCKET_MSG_GET_USER:
+                getUser(session, param);
+                break;
+            case WEBSOCKET_MSG_GET_ROLE_LIST:
+                getRoleList(session, param);
+                break;
+            case WEBSOCKET_MSG_GET_ORDER_LIST:
+                getOrderList(session, param);
+                break;
+            case WEBSOCKET_MSG_QUICK_REPLY:
+                quickReply(session, param);
+                break;
+            default:
+                this.sendMessage(session, KfWebSocketMsgDTO.fail(webSocketMsgType, "参数错误, 未知的消息类型"));
+        }
+    }
+
+    /**
+     * 消息传输发生错误
+     */
+    @Override
+    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
+    }
+
+    /**
+     * 连接被关闭
+     */
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
+        //关闭连接
+        session.close();
+        //判断会话是否保存
+        Object kfUserIdObject = session.getAttributes().get("kfUserId");
+        if (kfUserIdObject == null) {
+            return;
+        }
+        //从session中获取客服id
+        Long kfUserId = Long.valueOf(kfUserIdObject.toString());
+        //移除连接
+        kfMsgWebSocketSessionRegistry.removeSession(kfUserId);
+    }
+
+    @Override
+    public boolean supportsPartialMessages() {
+        return false;
+    }
+
+    private void quickReply(WebSocketSession session, KfWebSocketMsgParam param) {
+        KfWebSocketMsgParam.QuickReplyBean quickReplyBean = param.getQuickReplyBean();
+        if (quickReplyBean == null) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(),
+                    "快捷回复参数错误, 参数对象不可为空, param : " + JsonUtil.toString(param)));
+            return;
+        }
+        //查询
+        if (Objects.equals(quickReplyBean.getKfOperateEnum(), KfOperateEnum.KF_OPERATE_SELECT)) {
+            Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.QuickReplyBean>> tuple2 = kfQuickReplyService
+                    .quickReplyList(param.getPage());
+            this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                    .webSocketMsgType(param.getWebSocketMsgType())
+                    .page(tuple2.getT1())
+                    .quickReplyList(tuple2.getT2())
+                    .build());
+            return;
+        }
+        //处理不同类型的消息
+        Boolean result = null;
+        switch (quickReplyBean.getKfOperateEnum()) {
+            case KF_OPERATE_ADD:
+                result = kfQuickReplyService.quickReplyAdd(param.getQuickReplyBean());
+                break;
+            case KF_OPERATE_DELETE:
+                result = kfQuickReplyService.quickReplyDelete(param.getQuickReplyBean());
+                break;
+            case KF_OPERATE_UPDATE:
+                result = kfQuickReplyService.quickReplyUpdate(param.getQuickReplyBean());
+                break;
+            default:
+        }
+        if (result != null && result) {
+            this.sendMessage(session, KfWebSocketMsgDTO.ok(param.getWebSocketMsgType()));
+            return;
+        }
+        this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(),
+                "快捷回复操作失败, param : " + JsonUtil.toString(param)));
+    }
+
+    private void getUser(WebSocketSession session, KfWebSocketMsgParam param) {
+        if (Strings.isBlank(param.getOpenId())) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(),
+                    "获取玩家信息参数错误, openId不可为空, param : " + JsonUtil.toString(param)));
+            return;
+        }
+        KfWebSocketMsgDTO.UserBean userBean = kfRoomService.getUserBean(param.getOpenId());
+        this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(param.getWebSocketMsgType())
+                .kfUserId(SecurityUtil.getUserId())
+                .user(userBean)
+                .build());
+    }
+
+    private void getRoleList(WebSocketSession session, KfWebSocketMsgParam param) {
+        if (Strings.isBlank(param.getOpenId())) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(),
+                    "获取玩家角色列表参数错误, openId不可为空, param : " + JsonUtil.toString(param)));
+            return;
+        }
+        Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.GameRoleBean>> tuple2 = kfRoomService
+                .getRoleBeanList(param.getOpenId(), param.getPage());
+        this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(param.getWebSocketMsgType())
+                .kfUserId(SecurityUtil.getUserId())
+                .page(tuple2.getT1())
+                .roleList(tuple2.getT2())
+                .build());
+    }
+
+    private void getOrderList(WebSocketSession session, KfWebSocketMsgParam param) {
+        if (Strings.isBlank(param.getOpenId())) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(),
+                    "获取玩家订单列表参数错误, openId不可为空, param : " + JsonUtil.toString(param)));
+            return;
+        }
+        Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.OrderBean>> tuple2 = kfRoomService
+                .getOrderBeanList(param.getOpenId(), param.getPage());
+        this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(param.getWebSocketMsgType())
+                .kfUserId(SecurityUtil.getUserId())
+                .page(tuple2.getT1())
+                .orderList(tuple2.getT2())
+                .build());
+    }
+
+    private void kfSendMsg(WebSocketSession session, KfWebSocketMsgParam param) {
+        if (param.getRoomId() == null) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(), "参数错误,roomId为空"));
+            return;
+        }
+        KfRoom kfRoom = kfRoomService.getById(param.getRoomId());
+        if (kfRoom == null) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(), "参数错误,房间信息不存在"));
+            return;
+        }
+        KfWebSocketMsgParam.MsgContentBean msgContent = param.getMsgContent();
+        if (msgContent == null) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(), "参数错误,消息内容不可为空"));
+            return;
+        }
+        //发送消息
+        Map<String, Object> msgParamMap = new HashMap<>(3);
+        msgParamMap.put("touser", kfRoom.getOpenId());
+        //文本
+        if (Objects.equals(msgContent.getMsgType(), KfRoomMsgTypeEnum.KF_MSG_TYPE_TEXT)) {
+            Map<String, Object> textMap = new HashMap<>(1);
+            textMap.put("content", msgContent.getText());
+            msgParamMap.put("msgtype", KfRoomMsgTypeEnum.KF_MSG_TYPE_TEXT.getValue());
+            msgParamMap.put(KfRoomMsgTypeEnum.KF_MSG_TYPE_TEXT.getValue(), textMap);
+        }
+        //图片
+        if (Objects.equals(msgContent.getMsgType(), KfRoomMsgTypeEnum.KF_MSG_TYPE_IMAGE)) {
+            Map<String, Object> imageMap = new HashMap<>(1);
+            //图片上传到腾讯, 转成 media_id
+            Tuple2<Long, String> tuple2 = wxApiService.mediaUpload(param.getGameId(),
+                    FileUtil.urlToMultipartFile(msgContent.getImage()));
+            //素材上传失败, 通知返回通知客户端
+            if (tuple2.getT1() != 0) {
+                this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(), tuple2.getT2()));
+                return;
+            }
+            imageMap.put("media_id", tuple2.getT2());
+            msgParamMap.put("msgtype", KfRoomMsgTypeEnum.KF_MSG_TYPE_IMAGE.getValue());
+            msgParamMap.put(KfRoomMsgTypeEnum.KF_MSG_TYPE_IMAGE.getValue(), imageMap);
+        }
+        //调腾讯接口发送消息
+        Tuple2<Long, String> tuple2 = wxApiService.sendCustomMessageApi(param.getGameId(), msgParamMap);
+        //发送失败, 通知返回通知客户端
+        if (tuple2.getT1() != 0) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(), tuple2.getT2()));
+            return;
+        }
+        //发送成功, 消息入库
+        kfRoomMsgService.sendMsgSave(param.getGameId(), kfRoom, msgContent);
+        this.sendMessage(session, KfWebSocketMsgDTO.ok(param.getWebSocketMsgType()));
+    }
+
+    private void kfFinishSession(WebSocketSession session, KfWebSocketMsgEnum webSocketMsgType, Long gameId, Long roomId) {
+        //房间在线状态更新
+        kfRoomService.update(new LambdaUpdateWrapper<KfRoom>()
+                .set(KfRoom::getOnline, Boolean.FALSE)
+                .set(KfRoom::getUpdateTime, LocalDateTime.now())
+                .eq(KfRoom::getId, roomId));
+        //推送完整的已链接房间列表
+        List<KfWebSocketMsgDTO.RoomBean> onlineRoomList = kfRoomService.getOnlineRoomList(gameId);
+        this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(webSocketMsgType)
+                .kfUserId(SecurityUtil.getUserId())
+                .gameId(gameId)
+                .roomList(onlineRoomList)
+                .build());
+    }
+
+    private void finishRoomList(WebSocketSession session, KfWebSocketMsgParam param) {
+        Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.RoomBean>> tuple2 = kfRoomService
+                .getFinishRoomList(param.getGameId(), param.getPage());
+        this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(param.getWebSocketMsgType())
+                .kfUserId(SecurityUtil.getUserId())
+                .gameId(param.getGameId())
+                .page(tuple2.getT1())
+                .roomList(tuple2.getT2())
+                .build());
+    }
+
+    private void msgRoomHistory(WebSocketSession session, KfWebSocketMsgParam param) {
+        if (param.getRoomId() == null) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(),
+                    "获取房间历史消息参数错误, roomId不可为空, param : " + JsonUtil.toString(param)));
+            return;
+        }
+        //分页获取房间消息列表
+        Tuple2<KfWebSocketMsgDTO.PageBean, List<KfWebSocketMsgDTO.RoomMsgBean>> tuple2 = kfRoomMsgService
+                .msgRoomHistory(param.getRoomId(), param.getPage());
+        //房间信息设置
+        List<KfWebSocketMsgDTO.RoomBean> roomList = kfRoomService.getRoomByRoomId(param.getRoomId());
+        //游戏列表
+        List<KfWebSocketMsgDTO.GameBean> gameList = kfRoomService.getKfGameByGameId(param.getGameId());
+        //发送消息
+        this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(param.getWebSocketMsgType())
+                .kfUserId(SecurityUtil.getUserId())
+                .page(tuple2.getT1())
+                .gameId(param.getGameId())
+                .roomId(param.getRoomId())
+                .gameList(gameList)
+                .roomList(roomList)
+                .roomMsgList(tuple2.getT2())
+                .build());
+    }
+
+    private void userConnectJoin(WebSocketSession session, KfWebSocketMsgParam param) {
+        if (Strings.isBlank(param.getOpenId())) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(),
+                    "接入玩家参数错误, openId不可为空, param : " + JsonUtil.toString(param)));
+            return;
+        }
+        //触发玩家接入线程锁
+        if (!distributedLockComponent.doLock(RedisKeyConstant.KF_MSG_USER_CONNECT_JOIN + param.getOpenId(),
+                0L, 1L, TimeUnit.MINUTES)) {
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(param.getWebSocketMsgType(),
+                    "玩家已被其他客服接入, param : " + JsonUtil.toString(param)));
+            return;
+        }
+        //玩家更新
+        kfSessionUserService.update(new LambdaUpdateWrapper<KfSessionUser>()
+                .set(KfSessionUser::getIsWait, Boolean.FALSE)
+                .set(KfSessionUser::getUpdateTime, LocalDateTime.now())
+                .eq(KfSessionUser::getOpenId, param.getOpenId())
+        );
+        //玩家信息
+        KfSessionUser kfSessionUser = kfSessionUserService.getById(param.getOpenId());
+        //房间更新
+        Long roomId = kfRoomService.userJoinRoom(param.getOpenId(), param.getGameId());
+        //玩家未读消息更新到房间
+        kfRoomMsgService.update(new LambdaUpdateWrapper<KfRoomMsg>()
+                .set(KfRoomMsg::getRoomId, roomId)
+                .set(KfRoomMsg::getUserId, kfSessionUser == null ? null : kfSessionUser.getUserId())
+                .set(KfRoomMsg::getKfUserId, SecurityUtil.getUserId())
+                .set(KfRoomMsg::getUpdateTime, LocalDateTime.now())
+                .eq(KfRoomMsg::getOpenId, param.getOpenId())
+                .eq(KfRoomMsg::getGameId, param.getGameId())
+                .ne(KfRoomMsg::getMsgOwner, KfRoomMsgOwnerEnum.KF_MSG_OWNER_KF.getValue())
+                .eq(KfRoomMsg::getReadStatus, Boolean.FALSE));
+        //发送消息, 给该客服返回完整的已接入房间列表
+        List<KfWebSocketMsgDTO.RoomBean> roomList = kfRoomService.getOnlineRoomList(param.getGameId());
+        this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(param.getWebSocketMsgType())
+                .kfUserId(SecurityUtil.getUserId())
+                .gameId(param.getGameId())
+                .roomList(roomList)
+                .build());
+        //发送消息, 给所有在线客服推送完整待接入列表
+        List<KfWebSocketMsgDTO.WaitUserBean> waitUserList = kfSessionUserService.getWaitUserList(param.getGameId());
+        this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(param.getWebSocketMsgType())
+                .gameId(param.getGameId())
+                .waitUserList(waitUserList)
+                .build());
+        //释放锁
+        distributedLockComponent.unlock(RedisKeyConstant.KF_MSG_USER_CONNECT_JOIN + param.getOpenId());
+    }
+
+    private void kfCreateConnect(WebSocketSession session, KfWebSocketMsgEnum msgTypeEnum, Long gameId) {
+        //获取待接入列表
+        List<KfWebSocketMsgDTO.WaitUserBean> waitUserList = kfSessionUserService.getWaitUserList(gameId);
+        //获取已接入房间列表
+        List<KfWebSocketMsgDTO.RoomBean> roomList = kfRoomService.getOnlineRoomList(gameId);
+        //发送消息
+        this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(msgTypeEnum)
+                .kfUserId(SecurityUtil.getUserId())
+                .gameId(gameId)
+                .waitUserList(waitUserList)
+                .roomList(roomList)
+                .build());
+    }
+
+    private void kfHandShake(WebSocketSession session, KfWebSocketMsgEnum msgTypeEnum) {
+        //获取游戏列表
+        List<KfWebSocketMsgDTO.GameBean> gameList = kfRoomService.getKfGameList();
+        //发送消息
+        this.sendMessage(session, KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(msgTypeEnum)
+                .kfUserId(SecurityUtil.getUserId())
+                .gameList(gameList)
+                .build());
+    }
+
+    private boolean paramCheck(WebSocketSession session, KfWebSocketMsgEnum webSocketMsgType, String token, Long gameId) {
+        //gameId参数为空
+        if (gameId == null) {
+            //心跳和握手消息, 不需要携带gameId
+            if (!Objects.equals(KfWebSocketMsgEnum.WEBSOCKET_MSG_KF_HAND_SHAKE, webSocketMsgType)
+                    && !Objects.equals(KfWebSocketMsgEnum.WEBSOCKET_MSG_QUICK_REPLY, webSocketMsgType)) {
+                this.sendMessage(session, KfWebSocketMsgDTO.fail(webSocketMsgType, "参数错误, gameId为空"));
+                return Boolean.FALSE;
+            }
+        }
+        //令牌为空
+        if (Strings.isBlank(token)) {
+            log.error("非法参数, token令牌和客服id不可为空");
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(webSocketMsgType, "非法参数, token令牌和kfUserId不可为空"));
+            return Boolean.FALSE;
+        }
+        //令牌验证
+        TokenInfo tokenInfo;
+        try {
+            tokenInfo = SecurityUtil.parseToken(token);
+        } catch (Exception e) {
+            log.error("token验证异常, token : {}, e : {}", token, e.getMessage());
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(webSocketMsgType, "token验证异常, " + e.getMessage()));
+            return Boolean.FALSE;
+        }
+        if (tokenInfo == null) {
+            log.error("参数错误, 令牌验证不通过, token : {}", token);
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(webSocketMsgType, "参数错误, 令牌验证不通过"));
+            return Boolean.FALSE;
+        }
+        //将token设置到当前线程
+        try {
+            SecurityUtil.fillToken(token);
+        } catch (Exception e) {
+            log.error("token刷新到本地线程失败, token : {}, e : {}", token, e.getMessage());
+            this.sendMessage(session, KfWebSocketMsgDTO.fail(webSocketMsgType, "token刷新到本地线程失败"));
+            return Boolean.FALSE;
+        }
+        //添加会话
+        WebSocketSession webSocketSession = kfMsgWebSocketSessionRegistry.getSession(SecurityUtil.getUserId());
+        if (webSocketSession == null) {
+            session.getAttributes().put("kfUserId", SecurityUtil.getUserId());
+            kfMsgWebSocketSessionRegistry.addSession(SecurityUtil.getUserId(), session);
+        }
+        //返回验证通过
+        return Boolean.TRUE;
+    }
+
+    /**
+     * session直接发送消息
+     *
+     * @param session : 会话对象
+     */
+    private void sendMessage(WebSocketSession session, KfWebSocketMsgDTO kfWebSocketMsgDTO) {
+        try {
+            session.sendMessage(new TextMessage(JsonUtil.toString(kfWebSocketMsgDTO)));
+        } catch (IOException ignored) {
+        }
+    }
+}

+ 56 - 0
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebsocketHeartbeat.java

@@ -0,0 +1,56 @@
+package com.zanxiang.game.module.manage.websocket;
+
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.zanxiang.game.module.manage.enums.KfWebSocketMsgEnum;
+import com.zanxiang.game.module.manage.pojo.dto.KfWebSocketMsgDTO;
+import com.zanxiang.module.util.JsonUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.List;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-03-05
+ * @description : Websocket心跳
+ */
+@Slf4j
+@Component
+public class KfMsgWebsocketHeartbeat {
+
+    @Autowired
+    private KfMsgWebSocketSessionRegistry kfMsgWebSocketSessionRegistry;
+
+    /**
+     * Websocket每20秒给客户端发送一次心跳
+     */
+    @Scheduled(cron = "0/20 * * * * ?")
+    public void sessionHeartbeat() {
+        List<WebSocketSession> sessionList = kfMsgWebSocketSessionRegistry.getAllSessions();
+        if (CollectionUtils.isEmpty(sessionList)) {
+            return;
+        }
+        sessionList.forEach(this::sendHeartbeat);
+    }
+
+    private void sendHeartbeat(WebSocketSession session) {
+        //会话不存在或者未开启
+        if (session == null || !session.isOpen()) {
+            return;
+        }
+        Object kfUserId = session.getAttributes().get("kfUserId");
+        KfWebSocketMsgDTO webSocketMsgDTO = KfWebSocketMsgDTO.builder()
+                .webSocketMsgType(KfWebSocketMsgEnum.WEBSOCKET_MSG_CONNECT_HEART_BEAT)
+                .kfUserId(kfUserId == null ? null : Long.valueOf(kfUserId.toString()))
+                .build();
+        try {
+            session.sendMessage(new TextMessage(JsonUtil.toString(webSocketMsgDTO)));
+        } catch (Exception e) {
+            log.error("给客户端发送心跳消息异常, kfUserId : {}, e : {}", JsonUtil.toString(kfUserId), e.getMessage());
+        }
+    }
+}

+ 5 - 0
game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/Game.java

@@ -132,4 +132,9 @@ public class Game implements Serializable {
      */
     private Boolean isCommonGame;
 
+    /**
+     * 应用系统类型, IOS, Android
+     */
+    private String appType;
+
 }

+ 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/KfQuickReply.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-03-05
+ * @description : ${description}
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@ToString
+@Builder
+@TableName("t_kf_quick_reply")
+public class KfQuickReply {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 客服id
+     */
+    private Long kfUserId;
+
+    /**
+     * 快捷回复
+     */
+    private String content;
+
+    /**
+     * 是否有效
+     */
+    private Boolean status;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 65 - 0
game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfRoom.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")
+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 userId;
+
+    /**
+     * 游戏id
+     */
+    private Long gameId;
+
+    /**
+     * 客服id
+     */
+    private Long kfUserId;
+
+    /**
+     * 房间在线状态
+     */
+    private Boolean online;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,91 @@
+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 = "msg_id", type = IdType.INPUT)
+    private String msgId;
+
+    /**
+     * 消息类型
+     */
+    private String msgType;
+
+    /**
+     * 游戏id
+     */
+    private Long gameId;
+
+    /**
+     * 玩家open_id
+     */
+    private String openId;
+
+    /**
+     * 玩家id
+     */
+    private Long userId;
+
+
+    /**
+     * 客服id
+     */
+    private Long kfUserId;
+
+    /**
+     * 已读状态
+     */
+    private Boolean readStatus;
+
+    /**
+     * 房间id
+     */
+    private Long roomId;
+
+    /**
+     * 消息归属
+     */
+    private String msgOwner;
+
+    /**
+     * 消息内容
+     */
+    private String content;
+
+    /**
+     * 源数据
+     */
+    private String source;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

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

@@ -0,0 +1,80 @@
+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 = "open_id", type = IdType.INPUT)
+    private String openId;
+
+    /**
+     * 游戏id
+     */
+    private Long gameId;
+
+    /**
+     * 玩家id
+     */
+    private Long userId;
+
+    /**
+     * 是否待接入
+     */
+    private Boolean isWait;
+
+    /**
+     * 开始等待时间
+     */
+    private LocalDateTime waitStartTime;
+
+    /**
+     * 最近角色id
+     */
+    private String lastRoleId;
+
+    /**
+     * 最近角色名称
+     */
+    private String lastRoleName;
+
+    /**
+     * 游戏服务器id,默认为0
+     */
+    private String serverId;
+
+    /**
+     * 所在服务器名称
+     */
+    private String serverName;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 12 - 0
game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfQuickReplyMapper.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.KfQuickReply;
+
+/**
+ * @author : lingfeng
+ * @time : 2024-03-05
+ * @description : ${description}
+ */
+public interface KfQuickReplyMapper extends BaseMapper<KfQuickReply> {
+}

+ 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> {
+}

+ 1 - 1
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/SDKApplication.java

@@ -23,7 +23,7 @@ public class SDKApplication {
 
     public static void main(String[] args) {
         SpringApplication.run(SDKApplication.class, args);
-        System.out.println("赞象SDK服务启动成功 <渠道变更不上线时长更新为60天> ( ´・・)ノ(._.`) \n" +
+        System.out.println("赞象SDK服务启动成功 <关闭订单查询配置优化> ( ´・・)ノ(._.`) \n" +
                 " ___________ _   __\n" +
                 "/  ___|  _  \\ | / /\n" +
                 "\\ `--.| | | | |/ / \n" +

+ 5 - 0
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/constant/RedisKeyConstant.java

@@ -57,4 +57,9 @@ public class RedisKeyConstant {
      */
     public static final String USER_CREATE = RedisKeyConstant.REDIS_PREFIX + "user_create";
 
+    /**
+     * 客服支付订单标记
+     */
+    public static final String GAME_CUSTOM_PAY_SIGN = "game_sdk_manage_custom_pay_sign_";
+
 }

+ 0 - 9
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/IGamePayStrategyService.java

@@ -2,7 +2,6 @@ package com.zanxiang.game.module.sdk.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.zanxiang.game.module.mybatis.entity.GamePayStrategy;
-import com.zanxiang.game.module.sdk.pojo.param.UserData;
 
 /**
  * 游戏策略 服务类接口
@@ -11,12 +10,4 @@ import com.zanxiang.game.module.sdk.pojo.param.UserData;
  * @date 2022-07-01 11:38
  */
 public interface IGamePayStrategyService extends IService<GamePayStrategy> {
-
-    /**
-     * 支付策略
-     *
-     * @param user 用户
-     * @return {@link Boolean}
-     */
-    Boolean paySwitch(UserData user);
 }

+ 10 - 0
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/IGamePayWayService.java

@@ -30,6 +30,16 @@ public interface IGamePayWayService extends IService<GamePayWay> {
      */
     GamePayWayDTO getGamePayWay(Long gameId, Long payWayId, Long payDeviceId);
 
+    /**
+     * 关闭订单获取支付配置,
+     *
+     * @param gameId      游戏id
+     * @param payWayId    支付方式id
+     * @param payDeviceId 支付设备id
+     * @return {@link GamePayWayDTO}
+     */
+    GamePayWayDTO closeOrderGetGamePayWay(Long gameId, Long payWayId, Long payDeviceId);
+
     /**
      * 游戏id列表
      *

+ 0 - 12
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/IKfLinkService.java

@@ -1,12 +0,0 @@
-package com.zanxiang.game.module.sdk.service;
-
-import com.baomidou.mybatisplus.extension.service.IService;
-import com.zanxiang.game.module.mybatis.entity.KfLink;
-
-/**
- * @author : lingfeng
- * @time : 2023-12-06
- * @description : 客服链接
- */
-public interface IKfLinkService extends IService<KfLink> {
-}

+ 20 - 122
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GameAppletServiceImpl.java

@@ -2,34 +2,34 @@ package com.zanxiang.game.module.sdk.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.zanxiang.game.module.base.ServerInfo;
 import com.zanxiang.game.module.base.pojo.dto.H5GameConfigDTO;
 import com.zanxiang.game.module.base.pojo.enums.HttpStatusEnum;
-import com.zanxiang.game.module.base.pojo.enums.PayDeviceEnum;
-import com.zanxiang.game.module.base.pojo.enums.PayWayEnum;
-import com.zanxiang.game.module.mybatis.entity.*;
+import com.zanxiang.game.module.base.rpc.IKfMsgRpc;
+import com.zanxiang.game.module.mybatis.entity.Game;
+import com.zanxiang.game.module.mybatis.entity.GameApplet;
+import com.zanxiang.game.module.mybatis.entity.GameExt;
 import com.zanxiang.game.module.mybatis.mapper.GameAppletMapper;
-import com.zanxiang.game.module.sdk.enums.OrderStateEnum;
-import com.zanxiang.game.module.sdk.pojo.dto.*;
+import com.zanxiang.game.module.sdk.pojo.dto.AppletMsgDTO;
+import com.zanxiang.game.module.sdk.pojo.dto.GameAppletDTO;
 import com.zanxiang.game.module.sdk.pojo.param.UserData;
 import com.zanxiang.game.module.sdk.pojo.vo.GameInitVO;
-import com.zanxiang.game.module.sdk.service.*;
-import com.zanxiang.game.module.sdk.service.api.WxApiService;
+import com.zanxiang.game.module.sdk.service.IGameAppletService;
+import com.zanxiang.game.module.sdk.service.IGameExtService;
+import com.zanxiang.game.module.sdk.service.IGameService;
 import com.zanxiang.game.module.sdk.service.pay.MiPayService;
 import com.zanxiang.game.module.sdk.util.SignUtil;
 import com.zanxiang.module.util.JsonUtil;
 import com.zanxiang.module.util.bean.BeanUtil;
 import com.zanxiang.module.util.exception.BaseException;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.dubbo.config.annotation.DubboReference;
 import org.apache.logging.log4j.util.Strings;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
-import org.springframework.web.client.RestTemplate;
-import org.springframework.web.util.UriComponentsBuilder;
 import reactor.util.function.Tuples;
 
-import java.math.BigDecimal;
-import java.net.URI;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
@@ -43,23 +43,11 @@ import java.util.Objects;
 @Service
 public class GameAppletServiceImpl extends ServiceImpl<GameAppletMapper, GameApplet> implements IGameAppletService {
 
-    @Autowired
-    private IUserService userService;
-
-    @Autowired
-    private IOrderService orderService;
-
-    @Autowired
-    private WxApiService wxApiService;
-
-    @Autowired
-    private RestTemplate restTemplate;
+    @DubboReference(providedBy = ServerInfo.SERVER_DUBBO_NAME)
+    private IKfMsgRpc kfMsgRpc;
 
-    @Autowired
-    private IGamePayWayService gamePayWayService;
-
-    @Autowired
-    private IPayApplicationService payApplicationService;
+    @Value("${payConfig.wxPay.customH5Url}")
+    private String customH5Url;
 
     @Autowired
     private IGameService gameService;
@@ -70,12 +58,6 @@ public class GameAppletServiceImpl extends ServiceImpl<GameAppletMapper, GameApp
     @Autowired
     private MiPayService miPayService;
 
-    @Autowired
-    private IKfLinkService kfLinkService;
-
-    @Value("${payConfig.wxPay.customH5Url}")
-    private String customH5Url;
-
     @Override
     public String appletMsgCheck(String appId, String signature, String timestamp, String nonce, String echoStr) throws Exception {
         GameAppletDTO gameAppletDTO = this.getByAppId(appId);
@@ -109,22 +91,16 @@ public class GameAppletServiceImpl extends ServiceImpl<GameAppletMapper, GameApp
         }
         //消息内容
         AppletMsgDTO appletMsgDTO = JsonUtil.toObj(postData, AppletMsgDTO.class);
-        //文本消息
-        if (Objects.equals(appletMsgDTO.getMsgType(), AppletMsgDTO.MSG_TYPE_TEXT)) {
-            //用户信息
-            UserDTO userDTO = userService.getUserByOpenId(gameAppletDTO.getGameId(), appletMsgDTO.getFromUserName());
-            //用户客服支付会话
-            if (Objects.equals(appletMsgDTO.getContent(), AppletMsgDTO.MSG_CONTENT_PAY)) {
-                return this.customPayMessage(gameAppletDTO, userDTO);
-            }
-            //非客服会话, 返回指定的客服链接
-            return this.customLinkMessage(gameAppletDTO, userDTO);
-        }
         //米大师支付回调事件
         if (Objects.equals(appletMsgDTO.getMsgType(), AppletMsgDTO.MSG_TYPE_EVENT)
                 && Objects.equals(appletMsgDTO.getEvent(), AppletMsgDTO.EVENT_MI_PAY_CALL_BACK)) {
             return this.miPayMessage(appletMsgDTO, gameAppletDTO);
         }
+        //消息转发到后台客服系统处理
+        try {
+            kfMsgRpc.appletMsg(postData);
+        } catch (Exception ignored) {
+        }
         //其他消息不处理, 直接返回成功
         return result;
     }
@@ -176,84 +152,6 @@ public class GameAppletServiceImpl extends ServiceImpl<GameAppletMapper, GameApp
         return Objects.equals(miPayNotifyResult, Boolean.TRUE) ? successResult : failResult;
     }
 
-    private String customPayMessage(GameAppletDTO gameAppletDTO, UserDTO userDTO) {
-        //查询用户最新客服支付订单
-        Order order = orderService.getOne(new LambdaQueryWrapper<Order>()
-                .eq(Order::getUserId, userDTO.getId())
-                .eq(Order::getStatus, OrderStateEnum.READY_PAY.getCode())
-                .eq(Order::getPayWayId, PayWayEnum.WX_PAY.getPayWayId())
-                .eq(Order::getPayDeviceId, PayDeviceEnum.CUSTOM_PAY.getPayDeviceId())
-                .orderByDesc(Order::getCreateTime)
-                .last("limit 1"));
-        if (order != null) {
-            //发送客服消息
-            return this.sendCustomMessage(gameAppletDTO, userDTO.getOpenId(), order);
-        }
-        return HttpStatusEnum.SUCCESS.getMsg();
-    }
-
-    private String customLinkMessage(GameAppletDTO gameAppletDTO, UserDTO userDTO) {
-        //判断是否配置了第三方客服链接
-        KfLink kfLink = kfLinkService.getOne(new LambdaQueryWrapper<KfLink>()
-                .eq(KfLink::getAppId, gameAppletDTO.getAppId()));
-        if (kfLink == null || Strings.isBlank(kfLink.getCustomLink())) {
-            return HttpStatusEnum.SUCCESS.getMsg();
-        }
-        //其他消息对象
-        Map<String, Object> textMap = new HashMap<>(1);
-        textMap.put("content", kfLink.getCustomLink());
-        return this.sendCustomMessageApi(gameAppletDTO, userDTO.getOpenId(), "text", textMap);
-    }
-
-    private String sendCustomMessage(GameAppletDTO gameAppletDTO, String openId, Order order) {
-        //查询订单支付方式
-        GamePayWayDTO gamePayWayDTO = gamePayWayService.getById(order.getGamePayWayId());
-        //查询支付应用信息
-        PayApplicationDTO payApplicationDTO = payApplicationService.getPayApplicationByPayBoxId(gamePayWayDTO.getPayBoxId());
-        //客服支付链接显示图片地址
-        String thumbUrl = gamePayWayDTO.getThumbUrl();
-        //支付配置参数判断
-        if (Strings.isBlank(thumbUrl)) {
-            log.error("客服消息卡片图片地址不存在, gamePayWayDTO : {}", JsonUtil.toString(gamePayWayDTO));
-            return HttpStatusEnum.SUCCESS.getMsg();
-        }
-        //订单金额
-        BigDecimal amount = order.getAmount();
-        //构造跳转链接url
-        URI url = UriComponentsBuilder.fromHttpUrl(this.customH5Url)
-                .queryParam("appId", payApplicationDTO.getAppId())
-                .queryParam("orderId", order.getOrderId())
-                .queryParam("amount", amount)
-                .queryParam("description", "购买" + amount + "元档充值")
-                .build().toUri();
-        //link参数构造
-        Map<String, Object> linkMap = new HashMap<>(4);
-        linkMap.put("title", "点我充值");
-        linkMap.put("description", "点我充值" + amount + "元,用于购买" + amount + "元档充值");
-        linkMap.put("url", url);
-        linkMap.put("thumb_url", thumbUrl);
-        //发送客服消息
-        return this.sendCustomMessageApi(gameAppletDTO, openId, "link", linkMap);
-    }
-
-    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 = wxApiService.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();
-    }
-
     @Override
     public GameAppletDTO getByGameId(Long gameId) {
         GameApplet gameApplet = super.getOne(new LambdaQueryWrapper<GameApplet>()

+ 0 - 19
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GamePayStrategyServiceImpl.java

@@ -1,17 +1,11 @@
 package com.zanxiang.game.module.sdk.service.impl;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.zanxiang.game.module.base.pojo.enums.StatusEnum;
 import com.zanxiang.game.module.mybatis.entity.GamePayStrategy;
 import com.zanxiang.game.module.mybatis.mapper.GameStrategyMapper;
-import com.zanxiang.game.module.sdk.pojo.param.UserData;
 import com.zanxiang.game.module.sdk.service.IGamePayStrategyService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
-import org.springframework.util.CollectionUtils;
-
-import java.util.List;
 
 /**
  * 游戏策略 服务实现类
@@ -22,17 +16,4 @@ import java.util.List;
 @Slf4j
 @Service
 public class GamePayStrategyServiceImpl extends ServiceImpl<GameStrategyMapper, GamePayStrategy> implements IGamePayStrategyService {
-
-    @Override
-    public Boolean paySwitch(UserData user) {
-        List<GamePayStrategy> list = super.list(new LambdaQueryWrapper<GamePayStrategy>()
-                .eq(GamePayStrategy::getStatus, StatusEnum.YES.getCode())
-                .eq(GamePayStrategy::getGameId, user.getGameId())
-        );
-        if (CollectionUtils.isEmpty(list)) {
-            return false;
-        }
-        //存在切换则属于
-        return true;
-    }
 }

+ 13 - 0
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GamePayWayServiceImpl.java

@@ -50,6 +50,19 @@ public class GamePayWayServiceImpl extends ServiceImpl<GamePayWayMapper, GamePay
         return BeanUtil.copy(gamePayWay, GamePayWayDTO.class);
     }
 
+    @Override
+    public GamePayWayDTO closeOrderGetGamePayWay(Long gameId, Long payWayId, Long payDeviceId) {
+        GamePayWay gamePayWay = getOne(new LambdaQueryWrapper<GamePayWay>()
+                .eq(GamePayWay::getGameId, gameId)
+                .eq(GamePayWay::getPayWayId, payWayId)
+                .eq(GamePayWay::getPayDeviceId, payDeviceId));
+        if (gamePayWay == null) {
+            log.error("参数错误, 关闭订单游戏支付配置信息不存在, gameId : {}, payWayId : {}, payDeviceId : {}", gameId, payWayId, payDeviceId);
+            throw new BaseException("参数错误, 关闭订单游戏支付配置信息不存在");
+        }
+        return BeanUtil.copy(gamePayWay, GamePayWayDTO.class);
+    }
+
     @Override
     public List<GamePayWayDTO> listOfGameId(Long gameId) {
         List<GamePayWay> gamePayWayList = list(new LambdaQueryWrapper<GamePayWay>()

+ 2 - 1
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GameUserRoleServiceImpl.java

@@ -21,6 +21,7 @@ import com.zanxiang.module.util.JsonUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.kafka.clients.producer.Producer;
 import org.apache.kafka.clients.producer.ProducerRecord;
+import org.apache.logging.log4j.util.Strings;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
@@ -111,7 +112,7 @@ public class GameUserRoleServiceImpl extends ServiceImpl<GameUserRoleMapper, Gam
             return super.update(new LambdaUpdateWrapper<GameUserRole>()
                     .set(GameUserRole::getRoleName, param.getRoleName())
                     .set(GameUserRole::getRoleLevel, param.getRoleLevel())
-                    .set(GameUserRole::getServerId, param.getServerId())
+                    .set(gameUserRole == null || Strings.isBlank(gameUserRole.getServerId()), GameUserRole::getServerId, param.getServerId())
                     .set(GameUserRole::getServerName, param.getServerName())
                     .set(param.getRoleVipLevel() != null, GameUserRole::getRoleVipLevel, param.getRoleVipLevel())
                     .set(param.getRolePower() != null, GameUserRole::getRolePower, param.getRolePower())

+ 0 - 18
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/KfLinkServiceImpl.java

@@ -1,18 +0,0 @@
-package com.zanxiang.game.module.sdk.service.impl;
-
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.zanxiang.game.module.mybatis.entity.KfLink;
-import com.zanxiang.game.module.mybatis.mapper.KfLinkMapper;
-import com.zanxiang.game.module.sdk.service.IKfLinkService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-/**
- * @author : lingfeng
- * @time : 2023-12-06
- * @description : 客服链接
- */
-@Slf4j
-@Service
-public class KfLinkServiceImpl extends ServiceImpl<KfLinkMapper, KfLink> implements IKfLinkService {
-}

+ 12 - 0
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/OrderPayServiceImpl.java

@@ -6,6 +6,8 @@ import com.zanxiang.game.module.base.pojo.enums.PayDeviceEnum;
 import com.zanxiang.game.module.mybatis.entity.Order;
 import com.zanxiang.game.module.mybatis.entity.OrderPayParam;
 import com.zanxiang.game.module.mybatis.entity.User;
+import com.zanxiang.game.module.sdk.constant.RedisKeyConstant;
+import com.zanxiang.game.module.sdk.enums.ExpireTimeEnum;
 import com.zanxiang.game.module.sdk.enums.OrderStateEnum;
 import com.zanxiang.game.module.sdk.enums.PayTypeEnum;
 import com.zanxiang.game.module.sdk.pojo.dto.GamePayWayDTO;
@@ -16,6 +18,7 @@ import com.zanxiang.game.module.sdk.pojo.param.UserData;
 import com.zanxiang.game.module.sdk.pojo.vo.PayParamVO;
 import com.zanxiang.game.module.sdk.service.*;
 import com.zanxiang.game.module.sdk.service.pay.PayBaseService;
+import com.zanxiang.game.module.sdk.util.RedisUtil;
 import com.zanxiang.game.module.sdk.util.SpringUtils;
 import com.zanxiang.module.util.JsonUtil;
 import com.zanxiang.module.util.bean.BeanUtil;
@@ -58,6 +61,9 @@ public class OrderPayServiceImpl implements IOrderPayService {
     @Autowired
     private IUserService userService;
 
+    @Autowired
+    private RedisUtil<String> redisUtil;
+
     @Value("${payConfig.wxPay.customH5Url}")
     private String customH5Url;
 
@@ -183,6 +189,12 @@ public class OrderPayServiceImpl implements IOrderPayService {
             paramMap.put("description", "购买" + product.getAmount() + "元档充值");
             paramMap.put("serverUrl", this.serverUrl.contains("test") ? this.serverUrl + "/api/sdk" : this.serverUrl + "/sdk");
             log.error("下单参数返回, paramMap : {}", JsonUtil.toString(paramMap));
+            //客服支付添加redis缓存
+            if (Objects.equals(product.getPayDevice(), PayDeviceEnum.CUSTOM_PAY.getPayDeviceId())) {
+                //以玩家id为key, orderId为value, 设置5分钟缓存
+                redisUtil.setCache(RedisKeyConstant.GAME_CUSTOM_PAY_SIGN + user.getOpenId(),
+                        product.getOrderId(), ExpireTimeEnum.FIVE_MIN.getTime());
+            }
             return paramMap;
         }
         //创建支付参数

+ 4 - 1
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/UserServiceImpl.java

@@ -16,7 +16,10 @@ import com.zanxiang.game.module.sdk.pojo.param.UpdatePasswordParam;
 import com.zanxiang.game.module.sdk.pojo.param.UserData;
 import com.zanxiang.game.module.sdk.pojo.vo.CustomerVO;
 import com.zanxiang.game.module.sdk.pojo.vo.UserVO;
-import com.zanxiang.game.module.sdk.service.*;
+import com.zanxiang.game.module.sdk.service.IGameExtService;
+import com.zanxiang.game.module.sdk.service.ISmsService;
+import com.zanxiang.game.module.sdk.service.IUserCardService;
+import com.zanxiang.game.module.sdk.service.IUserService;
 import com.zanxiang.game.module.sdk.util.RegisterUtil;
 import com.zanxiang.module.util.JsonUtil;
 import com.zanxiang.module.util.bean.BeanUtil;

+ 1 - 1
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/pay/AliPayService.java

@@ -171,7 +171,7 @@ public class AliPayService extends PayBaseService {
     @Override
     public void closeOrder(PlatformOrderDTO platformOrderDTO) {
         //支付配置
-        GamePayWayDTO gamePayWayDTO = gamePayWayService.getGamePayWay(platformOrderDTO.getGameId(),
+        GamePayWayDTO gamePayWayDTO = gamePayWayService.closeOrderGetGamePayWay(platformOrderDTO.getGameId(),
                 platformOrderDTO.getPayWayId(), platformOrderDTO.getPayDeviceId());
         //初始化配置
         this.configInit(gamePayWayDTO);

+ 31 - 3
game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/pay/WxPayService.java

@@ -148,7 +148,13 @@ public class WxPayService extends PayBaseService {
             log.info("回调参数中attach值为空");
             return null;
         }
-        WxPayConfigDTO config = this.configInit(gamePayWayService.getById(attachBO.getGamePayWayId()));
+        //查询订单
+        PlatformOrderDTO platformOrderDTO = orderService.getByOrderId(packageParams.get("out_trade_no"));
+        if (platformOrderDTO == null) {
+            log.info("回调参数中订单id无法查询到订单信息");
+            return null;
+        }
+        WxPayConfigDTO config = this.notifyConfigInit(platformOrderDTO, gamePayWayService.getById(attachBO.getGamePayWayId()));
         // 账号信息
         String key = config.getApiKey();
         // 判断签名是否正确
@@ -279,10 +285,10 @@ public class WxPayService extends PayBaseService {
     @Override
     public void closeOrder(PlatformOrderDTO platformOrderDTO) {
         //支付配置
-        GamePayWayDTO gamePayWayDTO = gamePayWayService.getGamePayWay(platformOrderDTO.getGameId(),
+        GamePayWayDTO gamePayWayDTO = gamePayWayService.closeOrderGetGamePayWay(platformOrderDTO.getGameId(),
                 platformOrderDTO.getPayWayId(), platformOrderDTO.getPayDeviceId());
         //初始化配置
-        WxPayConfigDTO config = this.configInit(gamePayWayDTO);
+        WxPayConfigDTO config = this.notifyConfigInit(platformOrderDTO, gamePayWayDTO);
         try {
             Map<String, String> paramData = new HashMap<>(6);
             paramData.put("appid", config.getAppId());
@@ -379,4 +385,26 @@ public class WxPayService extends PayBaseService {
         //赋值配置信息
         return payConfigBO;
     }
+
+    private WxPayConfigDTO notifyConfigInit(PlatformOrderDTO platformOrderDTO, GamePayWayDTO gamePayWayDTO) {
+        //商户信息
+        PayMerchantDTO payMerchantDTO = payMerchantService.getByMerchantNo(platformOrderDTO.getMerchantNo());
+        //支付应用信息
+        PayApplicationDTO payApplicationDTO;
+        //小程序支付, 客服支付, 公众号支付, 关联了盒子
+        if (PayDeviceEnum.getByPayWayIdList().contains(gamePayWayDTO.getPayDeviceId())) {
+            payApplicationDTO = payApplicationService.getPayApplicationByPayBoxId(gamePayWayDTO.getPayBoxId());
+        } else {
+            payApplicationDTO = payApplicationService.getPayApplicationByAppId(gamePayWayDTO.getAppId());
+        }
+        //支付配置参数
+        WxPayConfigDTO payConfigBO = JsonUtil.toObj(payMerchantDTO.getPayConfig(), WxPayConfigDTO.class);
+        payConfigBO.setAppId(payApplicationDTO.getAppId());
+        payConfigBO.setAppSecret(payApplicationDTO.getAppSecret());
+        payConfigBO.setAppletType(payApplicationDTO.getType());
+        payConfigBO.setGamePayWayId(gamePayWayDTO.getId());
+        payConfigBO.setMachName(payMerchantDTO.getMerchantName());
+        //赋值配置信息
+        return payConfigBO;
+    }
 }