Browse Source

Merge branch 'package' of GameCenter/game-center into dev

zhimo 1 năm trước cách đây
mục cha
commit
119d407833
80 tập tin đã thay đổi với 5175 bổ sung1016 xóa
  1. 53 9
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/component/DataPowerComponent.java
  2. 16 0
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/controller/GameDataController.java
  3. 79 0
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/dto/FlowMonitorDTO.java
  4. 1 1
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/dto/GamePromoteDayDTO.java
  5. 1 1
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/dto/GamePromoteDayTotalDTO.java
  6. 19 0
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/dto/PlayerDataListDTO.java
  7. 7 0
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/FirstNewUserAgainTrendVO.java
  8. 35 0
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/FlowMonitorCountVo.java
  9. 126 0
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/FlowMonitorVO.java
  10. 3 0
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/PitcherDataDayTotalVO.java
  11. 3 0
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/PitcherDataDayVO.java
  12. 24 0
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/PlayerDataVO.java
  13. 14 0
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/IGameDataService.java
  14. 2 2
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/AccountAgentDayServiceImpl.java
  15. 13 5
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/AdsOrderDetailServiceImpl.java
  16. 436 432
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/GameDataServiceImpl.java
  17. 424 285
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/PitcherDataServiceImpl.java
  18. 68 6
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/PlayerDataServiceImpl.java
  19. 74 60
      game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/RoleManageServiceImpl.java
  20. 16 0
      game-module/game-module-base/src/main/java/com/zanxiang/game/module/base/rpc/IKfMsgRpc.java
  21. 15 0
      game-module/game-module-manage/pom.xml
  22. 1 1
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/ManageApplication.java
  23. 49 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/config/KfMsgWebSocketConfig.java
  24. 20 1
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/constant/RedisKeyConstant.java
  25. 1 1
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/ExcludeTagsEnum.java
  26. 39 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfOperateEnum.java
  27. 34 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfRoomMsgOwnerEnum.java
  28. 45 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfRoomMsgTypeEnum.java
  29. 89 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/KfWebSocketMsgEnum.java
  30. 44 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/enums/OrderStateEnum.java
  31. 72 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/KfAppletMsgDTO.java
  32. 48 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/KfUploadTempMediaDTO.java
  33. 617 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/dto/KfWebSocketMsgDTO.java
  34. 107 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/pojo/params/KfWebSocketMsgParam.java
  35. 29 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/rpc/impl/KfMsgRpcImpl.java
  36. 17 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfAppletMsgService.java
  37. 49 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfQuickReplyService.java
  38. 37 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfRoomMsgService.java
  39. 92 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfRoomService.java
  40. 23 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IKfSessionUserService.java
  41. 0 9
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IOssService.java
  42. 8 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/IPayApplicationService.java
  43. 115 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/api/KfWxApiService.java
  44. 107 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/CpCallServiceImpl.java
  45. 363 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfAppletMsgServiceImpl.java
  46. 82 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfQuickReplyServiceImpl.java
  47. 91 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfRoomMsgServiceImpl.java
  48. 285 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfRoomServiceImpl.java
  49. 82 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/KfSessionUserServiceImpl.java
  50. 0 16
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/OssServiceImpl.java
  51. 16 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/service/impl/PayApplicationServiceImpl.java
  52. 91 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/utils/FileUtil.java
  53. 64 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgRedisListener.java
  54. 35 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebSocketSessionRegistry.java
  55. 493 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebsocketHandler.java
  56. 56 0
      game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/websocket/KfMsgWebsocketHeartbeat.java
  57. 5 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/Game.java
  58. 2 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfLink.java
  59. 55 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfQuickReply.java
  60. 65 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfRoom.java
  61. 91 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfRoomMsg.java
  62. 80 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/entity/KfSessionUser.java
  63. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfQuickReplyMapper.java
  64. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfRoomMapper.java
  65. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfRoomMsgMapper.java
  66. 12 0
      game-module/game-module-mybatis/src/main/java/com/zanxiang/game/module/mybatis/mapper/KfSessionUserMapper.java
  67. 1 1
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/SDKApplication.java
  68. 5 0
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/constant/RedisKeyConstant.java
  69. 0 9
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/IGamePayStrategyService.java
  70. 10 0
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/IGamePayWayService.java
  71. 0 12
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/IKfLinkService.java
  72. 20 122
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GameAppletServiceImpl.java
  73. 0 19
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GamePayStrategyServiceImpl.java
  74. 13 0
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GamePayWayServiceImpl.java
  75. 2 1
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/GameUserRoleServiceImpl.java
  76. 0 18
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/KfLinkServiceImpl.java
  77. 12 0
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/OrderPayServiceImpl.java
  78. 4 1
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/impl/UserServiceImpl.java
  79. 1 1
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/pay/AliPayService.java
  80. 31 3
      game-module/game-module-sdk/src/main/java/com/zanxiang/game/module/sdk/service/pay/WxPayService.java

+ 53 - 9
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/component/DataPowerComponent.java

@@ -14,12 +14,16 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.dubbo.config.annotation.DubboReference;
+import org.nutz.dao.Cnd;
+import org.nutz.dao.Dao;
+import org.nutz.dao.Sqls;
+import org.nutz.dao.sql.Criteria;
+import org.nutz.dao.sql.Sql;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
 
 @Slf4j
 @Component
@@ -31,6 +35,9 @@ public class DataPowerComponent {
     @DubboReference(providedBy = ServerInfo.SERVER_DUBBO_NAME)
     private GameAuthRpc gameAuthRpc;
 
+    @Autowired
+    private Dao dao;
+
     public Tuple2<List<Long>, List<Long>> getPowerInfo() {
         return getPowerInfo("ZX_ONE");
     }
@@ -54,7 +61,9 @@ public class DataPowerComponent {
             return Tuple2.with(new ArrayList<>(subUserIds), null);
         }
         GameAuthUserVO userGameInfo = gameAuthRpc.getGameAuthByUserIds().getData();
-        if (CollectionUtils.isEmpty(userGameInfo.getGameIdList())) {
+        //原有的游戏ID添加通包游戏的权限
+        List<Long> gameIds = getFullGameIdList(userGameInfo.getGameIdList());
+        if (CollectionUtils.isEmpty(gameIds)) {
             throw new BaseException("没有游戏查看权限,请联系管理员指派游戏权限");
         }
         GameAuthEnum gameAuth = userGameInfo.getGameAuthEnum();
@@ -66,22 +75,22 @@ public class DataPowerComponent {
                 return Tuple2.with(null, null);
             } else if (gameAuth == GameAuthEnum.OPERATE || gameAuth == GameAuthEnum.CUSTOMER || gameAuth == GameAuthEnum.GS) {
                 // 运营
-                return Tuple2.with(null, userGameInfo.getGameIdList());
+                return Tuple2.with(null, gameIds);
             } else {
                 // 投手
-                return Tuple2.with(new ArrayList<>(subUserIds), userGameInfo.getGameIdList());
+                return Tuple2.with(new ArrayList<>(subUserIds), gameIds);
             }
         } else {
             if (gameAuth == GameAuthEnum.MANAGE) {
                 return Tuple2.with(null, null);
             } else if (gameAuth == GameAuthEnum.OPERATE || gameAuth == GameAuthEnum.CUSTOMER || gameAuth == GameAuthEnum.GS) {
                 // 运营组长
-                return Tuple2.with(null, userGameInfo.getGameIdList());
+                return Tuple2.with(null, gameIds);
             } else {
                 // 投手组长
                 // 自然量
                 subUserIds.add(0L);
-                return Tuple2.with(new ArrayList<>(subUserIds), userGameInfo.getGameIdList());
+                return Tuple2.with(new ArrayList<>(subUserIds), gameIds);
             }
         }
     }
@@ -127,4 +136,39 @@ public class DataPowerComponent {
         }
         return gameIdList;
     }
+
+    /**
+     * 获取全量的游戏ID(补全虚拟的IOS包游戏ID)
+     * @param gameIds 用户拥有的游戏权限的游戏ID
+     * @return 游戏ID
+     */
+    public List<Long> getFullGameIdList(List<Long> gameIds) {
+        //查询条件
+        Criteria cri = Cnd.cri();
+        if (CollectionUtils.isEmpty(gameIds)) {
+            return null;
+        } else {
+            cri.where().andInList("id", gameIds);
+        }
+        cri.where().andEquals("source_system", "ZX_ONE");
+        cri.where().andEquals("is_common_game", 1);
+        Sql sql = Sqls.create("""
+                SELECT
+                    guide_game_ids
+                FROM dm_game_order.t_game
+                """ + cri);
+        sql.setCallback(Sqls.callback.strList());
+        dao.execute(sql);
+        List<String> strList = sql.getList(String.class);
+        if (CollectionUtils.isEmpty(strList)) {
+            return gameIds;
+        }
+        for (String str : strList) {
+            gameIds.addAll(Arrays.stream(str.split(",")).
+                    map(gameId -> Long.parseLong(gameId) + 10000).toList());
+        }
+        return gameIds;
+    }
+
+
 }

+ 16 - 0
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/controller/GameDataController.java

@@ -139,4 +139,20 @@ public class GameDataController {
         return ResultVO.ok(activeDataService.getActiveDataTotal(dto));
     }
 
+    @ApiOperation(value = "流水监控")
+    @PreAuthorize(permissionKey = "gameData:flow:monitor")
+    @PostMapping("/flow/monitor")
+    public ResultVO<Page<FlowMonitorVO>> getFlowMonitor(@RequestBody FlowMonitorDTO dto) {
+        return ResultVO.ok(gameDataService.getFlowMonitor(dto));
+    }
+
+    @ApiOperation(value = "流水监控总计")
+    @PreAuthorize(permissionKey = "gameData:flow:monitorCount")
+    @PostMapping("/flow/monitorCount")
+    public ResultVO<FlowMonitorCountVo> getFlowMonitorCount(@RequestBody FlowMonitorDTO dto) {
+        return ResultVO.ok(gameDataService.getFlowMonitorCount(dto));
+    }
+
+
+
 }

+ 79 - 0
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/dto/FlowMonitorDTO.java

@@ -0,0 +1,79 @@
+package com.zanxiang.game.data.serve.pojo.dto;
+
+import com.zanxiang.game.data.serve.pojo.base.BasePage;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * @author ZhangXianyu
+ * @time 2024-03-04
+ * @Description 流水监控条件dto
+ **/
+@Data
+public class FlowMonitorDTO extends BasePage implements Serializable  {
+
+    /**
+     * SDK类型
+     */
+    @ApiModelProperty(value = "SDK类型")
+    private String sourceSystem;
+    /**
+     * 数据归因:1-子游戏维度; 2-父游戏维度
+     */
+    @ApiModelProperty(notes = "数据归因:1-子游戏维度; 2-父游戏维度")
+    private Long gameDimension = 1L;
+    /**
+     * 投手id
+     */
+    @ApiModelProperty(value = "投手id")
+    private List<Long> pitcherId;
+
+    /**
+     * 消耗开始时间
+     */
+    @ApiModelProperty(value = "消耗开始时间")
+    private LocalDate costBeginDate;
+
+    /**
+     * 消耗结束时间
+     */
+    @ApiModelProperty(value = "消耗结束时间")
+    private LocalDate costEndDate;
+
+    /**
+     * 订单开始时间
+     */
+    @ApiModelProperty(value = "订单开始时间")
+    private LocalDate orderBeginDate;
+
+    /**
+     * 订单结时间
+     */
+    @ApiModelProperty(value = "订单结时间")
+    private LocalDate orderEndDate;
+
+    /**
+     * 渠道id
+     */
+    @ApiModelProperty(value = "渠道id")
+    private List<Long> agentId;
+
+    /**
+     * 游戏id
+     */
+    @ApiModelProperty(value = "游戏id")
+    private List<Long> gameId;
+
+    /**
+     * 广告id
+     */
+    @ApiModelProperty(value = "广告id")
+    private List<Long> accountId;
+
+
+
+}

+ 1 - 1
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/dto/GamePromoteDayDTO.java

@@ -31,7 +31,7 @@ public class GamePromoteDayDTO extends BasePage {
     private String cpName;
 
     @ApiModelProperty(notes = "游戏id")
-    private List<Long> gameId;
+    private Long gameId;
     @ApiModelProperty(notes = "游戏名")
     private String gameName;
     @ApiModelProperty(notes = "游戏应用类型")

+ 1 - 1
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/dto/GamePromoteDayTotalDTO.java

@@ -29,7 +29,7 @@ public class GamePromoteDayTotalDTO {
     private String cpName;
 
     @ApiModelProperty(notes = "游戏id")
-    private List<Long> gameId;
+    private Long gameId;
 
     @ApiModelProperty(notes = "游戏名")
     private String gameName;

+ 19 - 0
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/dto/PlayerDataListDTO.java

@@ -7,6 +7,7 @@ import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.List;
 
 /**
@@ -183,4 +184,22 @@ public class PlayerDataListDTO extends BasePage {
     @ApiModelProperty(value = "玩家最近游戏角色名称")
     private String roleName;
 
+    /**
+     * 玩家最新染色时间
+     */
+    @ApiModelProperty(notes = "玩家最新染色时间")
+    private LocalDate userLastRegTime;
+
+    /**
+     * 玩家最新染色渠道ID
+     */
+    @ApiModelProperty(notes = "玩家最新染色渠道ID")
+    private Long userLastRegAgentId;
+
+    /**
+     * 玩家最新染色归因投手
+     */
+    @ApiModelProperty(notes = "玩家最新染色归因投手")
+    private Long userLastPitcherId;
+
 }

+ 7 - 0
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/FirstNewUserAgainTrendVO.java

@@ -1,5 +1,6 @@
 package com.zanxiang.game.data.serve.pojo.vo;
 
+import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -62,4 +63,10 @@ public class FirstNewUserAgainTrendVO {
     @ApiModelProperty(notes = "数据归因:1-子游戏维度; 2-父游戏维度")
     private Long gameDimension;
 
+    /**
+     * 投手id(用于投手每日查询用户详情)
+     */
+    @ApiModelProperty(value = "投手Id")
+    private String pitcherId;
+
 }

+ 35 - 0
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/FlowMonitorCountVo.java

@@ -0,0 +1,35 @@
+package com.zanxiang.game.data.serve.pojo.vo;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author ZhangXianyu
+ * @time 2024-03-05
+ * @Description 流水监控总计vo
+ **/
+@Data
+public class FlowMonitorCountVo implements Serializable {
+
+    /**
+     * 消耗总计
+     */
+    @ApiModelProperty(value = "消耗总计")
+    private Double costCount = 0.0;
+
+    /**
+     * 充值总计
+     */
+    @ApiModelProperty(value = "充值总计")
+    private Double amountCount = 0.0;
+
+    /**
+     * 回报率总计
+     */
+    @ApiModelProperty(value = "回报率总计")
+    private Double recoveryCount = 0.0;
+
+
+}

+ 126 - 0
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/FlowMonitorVO.java

@@ -0,0 +1,126 @@
+package com.zanxiang.game.data.serve.pojo.vo;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDate;
+
+/**
+ * @author ZhangXianyu
+ * @time 2024-03-04
+ * @Description 流水监控vo
+ **/
+@Data
+public class FlowMonitorVO implements Serializable {
+
+    /**
+     * SDK类型
+     */
+    @ApiModelProperty(value = "SDK类型")
+    private String sourceSystem;
+    /**
+     * 投手名称
+     */
+    @ApiModelProperty(value = "投手名称")
+    private String pitcherName;
+
+    /**
+     * 消耗开始时间
+     */
+    @ApiModelProperty(value = "消耗开始时间")
+    private LocalDate costBeginDate;
+
+    /**
+     * 消耗结束时间
+     */
+    @ApiModelProperty(value = "消耗结束时间")
+    private LocalDate costEndDate;
+
+    /**
+     * 订单时间
+     */
+    @ApiModelProperty(value = "订单时间")
+    private LocalDate orderDate;
+
+    /**
+     * 10点充值金额
+     */
+    @ApiModelProperty(value = "10点充值金额")
+    private Double tenAmount = 0.0;
+    /**
+     * 10点环比
+     */
+    @ApiModelProperty(value = "10点环比")
+    private Double tenRate = 0.0;
+    /**
+     * 10点回收率
+     */
+    @ApiModelProperty(value = "10点回收率")
+    private Double tenRecovery = 0.0;
+    /**
+     * 14点充值金额
+     */
+    @ApiModelProperty(value = "14点充值金额")
+    private Double fourteenAmount = 0.0;
+    /**
+     * 14点环比
+     */
+    @ApiModelProperty(value = "14点环比")
+    private Double fourteenRate = 0.0;
+    /**
+     * 14点回收率
+     */
+    @ApiModelProperty(value = "14点回收率")
+    private Double fourteenRecovery = 0.0;
+    /**
+     * 17点充值金额
+     */
+    @ApiModelProperty(value = "17点充值金额")
+    private Double seventeenAmount = 0.0;
+    /**
+     * 17点环比
+     */
+    @ApiModelProperty(value = "17点环比")
+    private Double seventeenRate = 0.0;
+    /**
+     * 17点回收率
+     */
+    @ApiModelProperty(value = "17点回收率")
+    private Double seventeenRecovery = 0.0;
+    /**
+     * 24点充值金额
+     */
+    @ApiModelProperty(value = "24点充值金额")
+    private Double twentyfourAmount = 0.0;
+    /**
+     * 24点环比
+     */
+    @ApiModelProperty(value = "24点环比")
+    private Double twentyfourRate = 0.0;
+    /**
+     * 24点回收率
+     */
+    @ApiModelProperty(value = "24点回收率")
+    private Double twentyfourRecovery = 0.0;
+
+    /**
+     * 消耗
+     */
+    @ApiModelProperty(value = "消耗")
+    private Double costCount = 0.0;
+
+    /**
+     * 游戏名称
+     */
+    @ApiModelProperty(value = "游戏名称")
+    private String gameName;
+
+    /**
+     * 渠道名称
+     */
+    @ApiModelProperty(value = "渠道名称")
+    private String agentName;
+
+}

+ 3 - 0
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/PitcherDataDayTotalVO.java

@@ -2126,4 +2126,7 @@ public class PitcherDataDayTotalVO {
     @ApiModelProperty(value = "付费趋势总:增/回/倍")
     private RechargeTrendVO amountSumTrend;
 
+    @ApiModelProperty(value = "da1的用户详情")
+    private FirstNewUserAgainTrendVO userDetails;
+
 }

+ 3 - 0
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/PitcherDataDayVO.java

@@ -2151,4 +2151,7 @@ public class PitcherDataDayVO {
     @ApiModelProperty(value = "付费趋势总:增/回/倍")
     private RechargeTrendVO amountSumTrend;
 
+    @ApiModelProperty(value = "da1的用户详情")
+    private FirstNewUserAgainTrendVO userDetails;
+
 }

+ 24 - 0
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/pojo/vo/PlayerDataVO.java

@@ -194,4 +194,28 @@ public class PlayerDataVO {
     @ApiModelProperty(notes = "关联建立时间")
     private LocalDateTime relationCreateTime;
 
+    /**
+     * 玩家最新染色时间
+     */
+    @ApiModelProperty(notes = "玩家最新染色时间")
+    private LocalDateTime userLastRegTime;
+
+    /**
+     * 玩家最新染色渠道ID
+     */
+    @ApiModelProperty(notes = "玩家最新染色渠道ID")
+    private Long userLastRegAgentId;
+
+    /**
+     * 玩家最新染色渠道名字
+     */
+    @ApiModelProperty(notes = "玩家最新染色渠道名字")
+    private String userLastAgentName;
+
+    /**
+     * 玩家最新染色归因投手
+     */
+    @ApiModelProperty(notes = "玩家最新染色归因投手")
+    private String userLastPitcherName;
+
 }

+ 14 - 0
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/IGameDataService.java

@@ -89,4 +89,18 @@ public interface IGameDataService {
      * @return
      */
     Page<GameDataUserDetailsVO> getUserDetails(GameDataUserDetailsDTO dto);
+
+    /**
+     * 流水监控
+     * @param dto
+     * @return
+     */
+    Page<FlowMonitorVO> getFlowMonitor(FlowMonitorDTO dto);
+
+    /**
+     * 流水监控总计
+     * @param dto
+     * @return
+     */
+    FlowMonitorCountVo getFlowMonitorCount(FlowMonitorDTO dto);
 }

+ 2 - 2
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/AccountAgentDayServiceImpl.java

@@ -54,7 +54,7 @@ public class AccountAgentDayServiceImpl implements IAccountAgentDayService {
     public Page<GamePromoteDayVO> accountAgentDay(GamePromoteDayDTO dto) {
         Tuple2<List<Long>, List<Long>> poerInfo = dataPowerComponent.getPowerInfo(dto.getSourceSystem());
         List<Long> userIds = StringUtils.isBlank(dto.getSysUserId()) ? poerInfo.first : List.of(Long.valueOf(dto.getSysUserId()));
-        List<Long> gameIds = CollectionUtils.isEmpty(dto.getGameId()) ? poerInfo.second : dto.getGameId();
+        List<Long> gameIds = dto.getGameId() == null ? poerInfo.second : List.of(dto.getGameId());
 
         if (null == dto.getBeginDay() || null == dto.getEndDay()) {
             dto.setBeginDay(LocalDate.now());
@@ -265,7 +265,7 @@ public class AccountAgentDayServiceImpl implements IAccountAgentDayService {
     public GamePromoteDayTotalVO accountAgentDayTotal(GamePromoteDayTotalDTO dto) {
         Tuple2<List<Long>, List<Long>> poerInfo = dataPowerComponent.getPowerInfo(dto.getSourceSystem());
         List<Long> userIds = StringUtils.isBlank(dto.getSysUserId()) ? poerInfo.first : List.of(Long.valueOf(dto.getSysUserId()));
-        List<Long> gameIds = CollectionUtils.isEmpty(dto.getGameId()) ? poerInfo.second : dto.getGameId();
+        List<Long> gameIds = dto.getGameId() == null ? poerInfo.second : List.of(dto.getGameId());
 
         if (null == dto.getBeginDay() || null == dto.getEndDay()) {
             dto.setBeginDay(LocalDate.now());

+ 13 - 5
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/AdsOrderDetailService.java → game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/AdsOrderDetailServiceImpl.java

@@ -2,7 +2,6 @@ package com.zanxiang.game.data.serve.service.impl;
 
 import com.github.sd4324530.jtuple.Tuple2;
 import com.google.common.base.CaseFormat;
-import com.zanxiang.erp.security.util.SecurityUtil;
 import com.zanxiang.game.data.serve.component.DataPowerComponent;
 import com.zanxiang.game.data.serve.pojo.dto.AdsOrderDetailDTO;
 import com.zanxiang.game.data.serve.pojo.dto.AdsOrderDetailListDTO;
@@ -34,7 +33,7 @@ import java.util.stream.Collectors;
 
 @Slf4j
 @Service
-public class AdsOrderDetailService implements IAdsOrderDetailService {
+public class AdsOrderDetailServiceImpl implements IAdsOrderDetailService {
 
     @Autowired
     private Dao dao;
@@ -420,7 +419,10 @@ public class AdsOrderDetailService implements IAdsOrderDetailService {
                  		l.id as back_id, -- 回传id
                  		l.back_table_name as back_table_name -- 回传表名
                 	FROM dm_game_order.t_game_order a
-                	LEFT JOIN dm_game_order.t_game_user b on a.source_system = b.source_system AND a.user_id = b.id
+                	LEFT JOIN dm_game_order.t_game_user_role ab
+                	on a.source_system = ab.source_system AND a.role_id = ab.role_id AND a.user_id = ab.user_id
+                	LEFT JOIN dm_game_order.t_game_user_burst b
+                	on a.source_system = b.source_system AND a.user_id = b.id AND ab.create_time >= b.create_time AND ab.create_time <b.end_time
                 	LEFT JOIN dm_game_order.t_pitcher_agent c on a.source_system = c.source_system AND a.agent_id = c.id
                 	LEFT JOIN dm_game_order.t_pitcher_map d on c.source_system = d.source_system AND c.pitcher_id = d.zx_pitcher_id
                 	LEFT JOIN dm_game_order.t_cp e on a.source_system = e.source_system AND a.cp_id = e.id
@@ -588,7 +590,10 @@ public class AdsOrderDetailService implements IAdsOrderDetailService {
                  		l.id as back_id, -- 回传id
                  		l.back_table_name as back_table_name -- 回传表名
                 	FROM dm_game_order.t_game_order a
-                	LEFT JOIN dm_game_order.t_game_user b on a.source_system = b.source_system AND a.user_id = b.id
+                	LEFT JOIN dm_game_order.t_game_user_role ab
+                	on a.source_system = ab.source_system AND a.role_id = ab.role_id AND a.user_id = ab.user_id
+                	LEFT JOIN dm_game_order.t_game_user_burst b
+                	on a.source_system = b.source_system AND a.user_id = b.id AND ab.create_time >= b.create_time AND ab.create_time <b.end_time
                 	LEFT JOIN dm_game_order.t_pitcher_agent c on a.source_system = c.source_system AND a.agent_id = c.id
                 	LEFT JOIN dm_game_order.t_pitcher_map d on c.source_system = d.source_system AND c.pitcher_id = d.zx_pitcher_id
                 	LEFT JOIN dm_game_order.t_cp e on a.source_system = e.source_system AND a.cp_id = e.id
@@ -758,7 +763,10 @@ public class AdsOrderDetailService implements IAdsOrderDetailService {
                  		l.id as back_id, -- 回传id
                  		l.back_table_name as back_table_name -- 回传表名
                 	FROM dm_game_order.t_game_order a
-                	LEFT JOIN dm_game_order.t_game_user b on a.source_system = b.source_system AND a.user_id = b.id
+                	LEFT JOIN dm_game_order.t_game_user_role ab
+                	on a.source_system = ab.source_system AND a.role_id = ab.role_id AND a.user_id = ab.user_id
+                	LEFT JOIN dm_game_order.t_game_user_burst b
+                	on a.source_system = b.source_system AND a.user_id = b.id AND ab.create_time >= b.create_time AND ab.create_time <b.end_time
                 	LEFT JOIN dm_game_order.t_pitcher_agent c on a.source_system = c.source_system AND a.agent_id = c.id
                 	LEFT JOIN dm_game_order.t_pitcher_map d on c.source_system = d.source_system AND c.pitcher_id = d.zx_pitcher_id
                 	LEFT JOIN dm_game_order.t_cp e on a.source_system = e.source_system AND a.cp_id = e.id

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 436 - 432
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/GameDataServiceImpl.java


+ 424 - 285
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/PitcherDataServiceImpl.java

@@ -14,6 +14,7 @@ import com.zanxiang.module.util.exception.BaseException;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.nutz.dao.Cnd;
 import org.nutz.dao.Dao;
@@ -202,6 +203,7 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
     public Page<PitcherDataDayVO> getPitcherDataDay(PitcherDataDayDTO dto) {
         com.github.sd4324530.jtuple.Tuple2<List<Long>, List<Long>> poerInfo = dataPowerComponent.getPowerInfo(dto.getSourceSystem());
         List<Long> userIds = CollectionUtils.isEmpty(dto.getPitcherId()) ? poerInfo.first : dto.getPitcherId();
+//        List<Long> userIds = CollectionUtils.isEmpty(dto.getPitcherId()) ? null : dto.getPitcherId();
 
         //不传递时间,默认查询当天
         if (dto.getBeginDate() == null || dto.getEndDate() == null) {
@@ -252,16 +254,130 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
         dao.execute(sql);
         //设置总记录数
         pager.setRecordCount(dao.count(clazz, cri));
+
+        //查询用户id
+        Map<String, String> map = findUsersIdForGameData(dto);
+
         //处理dayN数据
         List<PitcherDataDayVO> tempList = sql.getList(PitcherDataDayVO.class);
+        //取出用户ids,制作响应vo
         List<PitcherDataDayVO> list = tempList.stream().map(vo -> {
+            Long[] usersID = ArrayUtils.EMPTY_LONG_OBJECT_ARRAY;
+            //将pitcherId和需要查询的注册时间拼接
+            String pitcherIdDate = vo.getPitcherId() + "/" + dto.getBeginDate();
+            //取出所有usersId
+            if (map.containsKey(pitcherIdDate)) {
+                //string[] -> Long[]
+                usersID = Arrays.stream(map.get(pitcherIdDate)
+                                .split("/"))
+                        .map(Long::parseLong)
+                        .toArray(Long[]::new);
+            }
             formatPitcherDataDayDayN(vo);
+
+            FirstNewUserAgainTrendVO againTrendVO = FirstNewUserAgainTrendVO.builder()
+                    .gameDimension(dto.getGameDimension())
+                    .sourceSystem(dto.getSourceSystem())
+                    .timeType("da1")
+                    .rechargeCount(vo.getAmountD1Trend().getRechargeUserCount())
+                    .usersId(usersID)
+                    .build();
+            vo.setUserDetails(againTrendVO);
             return vo;
         }).collect(Collectors.toList());
         //返回结果
         return new Page<>(list, pager);
     }
 
+
+    /**
+     * 查询出注册当天且充值的所有用户id-投手每日
+     *
+     * @param dto 前端查询参数
+     * @return map
+     */
+    private Map findUsersIdForGameData(PitcherDataDayDTO dto) {
+        //不选投手查询的结果 --> (投手id/日期, userId)
+
+        //默认查询子游戏维度
+        if (dto.getGameDimension() == null) {
+            dto.setGameDimension(1L);
+        }
+        //默认查询字段
+        String gameColumn = "pitcher_id";
+        if (dto.getGameDimension() == 2L) {
+            gameColumn = "pitcher_id";
+        }
+        //查询每个游戏的注册首日充值用户id,在玩家充值排行榜里查找
+        Criteria findUsersCri = Cnd.cri();
+        //拼接游戏ID
+        if (CollectionUtils.isNotEmpty(dto.getPitcherId())) {
+            findUsersCri.where().andInList(gameColumn, dto.getPitcherId());
+        }
+        //拼接SDK来源
+        if (StringUtils.isNotBlank(dto.getSourceSystem())) {
+            findUsersCri.where().andEquals("source_system", dto.getSourceSystem());
+        }
+        //拼接查询时间
+        if (dto.getBeginDate() != null && dto.getEndDate() != null) {
+            findUsersCri.where().andBetween("DATE(reg_user_time)", dto.getBeginDate(), dto.getEndDate());
+        }
+        findUsersCri.where().andNotEquals("reg_agent_id", 0);
+        Sql findUsersIdSql;
+        Criteria groupByCri = Cnd.cri();
+        groupByCri.getGroupBy().groupBy("pitcher_id", "DATE(reg_user_time)");
+        findUsersIdSql = Sqls.create(findUsersIdSql(findUsersCri, groupByCri));
+        //自定义回传
+        findUsersIdSql.setCallback((connection, resultSet, sql) -> {
+            Map<String, String> tempMap = new HashMap<>();
+            while (resultSet.next()) {
+                tempMap.put(resultSet.getString(1), resultSet.getString(2));
+            }
+            return tempMap;
+        });
+        //运行sql
+        dao.execute(findUsersIdSql);
+        //返回结果
+        return findUsersIdSql.getObject(Map.class);
+    }
+
+    /**
+     * 找出每个游戏注册时间内的注册当天且充值的用户id-投手每日-子游戏维度 (不选投手)
+     *
+     * @return String
+     */
+    private String findUsersIdSql(Criteria cri, Criteria groupByCri) {
+        return """
+                SELECT
+                    CONCAT(pitcher_id,'/',DATE(reg_user_time)) as game_id_date,
+                    GROUP_CONCAT(CONVERT (player_id, varchar), "/") as amount_users_id
+                FROM
+                    game_ads.ads_player_recharge_ranking
+                """ + cri +
+                """
+                            and dt = DATE(reg_user_time) 
+                        """ + groupByCri;
+    }
+
+    /**
+     * 找出每个游戏注册时间内的注册当天且充值的用户id-投手每日-子游戏维度(选投手)
+     *
+     * @return String
+     */
+    private String findUsersIdSqlPitcher(Criteria cri, Criteria groupByCri) {
+        return """
+                SELECT
+                    DATE(reg_user_time) as reg_user_time,
+                    GROUP_CONCAT(CONVERT (player_id, varchar), "/") as amount_users_id
+                FROM
+                    game_ads.ads_player_recharge_ranking
+                """ + cri +
+                """
+                            and dt = DATE(reg_user_time) 
+                        """ + groupByCri;
+    }
+
+
     /**
      * 投手每日数据总计
      *
@@ -272,7 +388,7 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
     public PitcherDataDayTotalVO getPitcherDataDayTotal(PitcherDataDayTotalDTO dto) {
         com.github.sd4324530.jtuple.Tuple2<List<Long>, List<Long>> poerInfo = dataPowerComponent.getPowerInfo(dto.getSourceSystem());
         List<Long> userIds = CollectionUtils.isEmpty(dto.getPitcherId()) ? poerInfo.first : dto.getPitcherId();
-
+//        List<Long> userIds = CollectionUtils.isEmpty(dto.getPitcherId()) ? null : dto.getPitcherId();
         //不传递时间,默认查询当天
         if (dto.getBeginDate() == null || dto.getEndDate() == null) {
             dto.setBeginDate(LocalDate.now());
@@ -305,11 +421,34 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
         //执行sql
         dao.execute(sql);
         PitcherDataDayTotalVO vo = sql.getObject(PitcherDataDayTotalVO.class);
+
+        PitcherDataDayDTO pitcherDataDayDTO = new PitcherDataDayDTO();
+        BeanUtils.copyProperties(dto, pitcherDataDayDTO);
+        //查询用户id
+        Map<String, String> map = findUsersIdForGameData(pitcherDataDayDTO);
+
+        Long[] ids = ArrayUtils.EMPTY_LONG_OBJECT_ARRAY;
+
+        String idStr = map.values().stream().filter(string -> !string.isEmpty()).collect(Collectors.joining("/"));
+        if (StringUtils.isNotBlank(idStr)) {
+            ids = Arrays.stream(idStr.split("/")).map(Long::parseLong).toArray(Long[]::new);
+        }
+
+
         if (StringUtils.isNotBlank(vo.getAmountD1())) {
             //处理dayN数据
             formatPitcherDataDayTotalDayN(vo);
-        }
 
+            FirstNewUserAgainTrendVO againTrendVO = FirstNewUserAgainTrendVO.builder()
+                    .gameDimension(dto.getGameDimension())
+                    .sourceSystem(dto.getSourceSystem())
+                    .timeType("da1")
+                    .rechargeCount(vo.getAmountD1Trend().getRechargeUserCount())
+                    .usersId(ids)
+                    .build();
+
+            vo.setUserDetails(againTrendVO);
+        }
         return vo;
     }
 
@@ -323,7 +462,7 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
     public Page<PitcherDataTotalVO> getPitcherDataTotal(PitcherDataTotalDTO dto) {
         com.github.sd4324530.jtuple.Tuple2<List<Long>, List<Long>> poerInfo = dataPowerComponent.getPowerInfo(dto.getSourceSystem());
         List<Long> userIds = CollectionUtils.isEmpty(dto.getPitcherId()) ? poerInfo.first : dto.getPitcherId();
-
+//        List<Long> userIds = CollectionUtils.isEmpty(dto.getPitcherId()) ? null : dto.getPitcherId();
         //如果注册时间参数为空,默认设置查询当天数据
         if (dto.getBeginDate() == null || dto.getEndDate() == null) {
             dto.setBeginDate(LocalDate.now());
@@ -607,16 +746,16 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
         //设置总记录数
         Sql sqlCount = Sqls.create(
                 """
-                    SELECT
-                    	COUNT(1)
-                    FROM (
-                    	SELECT
-                    		COUNT(*)
-                    	FROM game_ads.ads_game_pitcher_day
-                """ + cri +
-                """
-                    ) a
-                """);
+                            SELECT
+                            	COUNT(1)
+                            FROM (
+                            	SELECT
+                            		COUNT(*)
+                            	FROM game_ads.ads_game_pitcher_day
+                        """ + cri +
+                        """
+                                    ) a
+                                """);
         sqlCount.setCallback(Sqls.callback.integer());
         dao.execute(sqlCount);
         pager.setRecordCount(sqlCount.getInt());
@@ -698,16 +837,16 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
         //设置总记录数
         Sql sqlCount = Sqls.create(
                 """
-                    SELECT
-                    	COUNT(1)
-                    FROM (
-                    	SELECT
-                    		COUNT(*)
-                    	FROM game_ads_parent.ads_game_pitcher_day_parent
-                """ + cri +
-                """
-                    ) a
-                """);
+                            SELECT
+                            	COUNT(1)
+                            FROM (
+                            	SELECT
+                            		COUNT(*)
+                            	FROM game_ads_parent.ads_game_pitcher_day_parent
+                        """ + cri +
+                        """
+                                    ) a
+                                """);
         sqlCount.setCallback(Sqls.callback.integer());
         dao.execute(sqlCount);
         pager.setRecordCount(sqlCount.getInt());
@@ -1029,14 +1168,14 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
         //账面人数数据
         Sql sqlAmountNum = Sqls.create(
                 """
-                SELECT
-                    IFNULL(COUNT(DISTINCT user_id), 0) amount_num
-                FROM
-                    game_ads.ads_information
-                """ + criAmount +
-                """
-                AND NOT agent_id = 0
-                """);
+                        SELECT
+                            IFNULL(COUNT(DISTINCT user_id), 0) amount_num
+                        FROM
+                            game_ads.ads_information
+                        """ + criAmount +
+                        """
+                                AND NOT agent_id = 0
+                                """);
         //设置回传对象
         sqlAmountNum.setCallback(Sqls.callback.longValue());
         //执行sql
@@ -1050,14 +1189,14 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
         //新用户充值人数
         Sql sqlNewUserNum = Sqls.create(
                 """
-                SELECT
-                    COUNT(DISTINCT user_id) new_user_amount_num
-                FROM
-                    game_ads.ads_information
-                """ + criNewUser +
-                """
-                AND NOT agent_id = 0
-                """);
+                        SELECT
+                            COUNT(DISTINCT user_id) new_user_amount_num
+                        FROM
+                            game_ads.ads_information
+                        """ + criNewUser +
+                        """
+                                AND NOT agent_id = 0
+                                """);
         //设置回传对象
         sqlNewUserNum.setCallback(Sqls.callback.longValue());
         //执行sql
@@ -1068,20 +1207,20 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
         //新用户复充人数
         Sql sqlNewUserAgainNum = Sqls.create(
                 """
-                SELECT
-                    COUNT(tempA.num) as new_user_order_again
-                FROM (
-                    SELECT
-                        COUNT(user_id) num
-                    FROM
-                        game_ads.ads_information
-                """ + criNewUser +
-                """
-                    AND NOT agent_id = 0
-                    GROUP BY user_id
-                    HAVING
-                        COUNT(user_id) > 1 ) tempA
-                """);
+                        SELECT
+                            COUNT(tempA.num) as new_user_order_again
+                        FROM (
+                            SELECT
+                                COUNT(user_id) num
+                            FROM
+                                game_ads.ads_information
+                        """ + criNewUser +
+                        """
+                                    AND NOT agent_id = 0
+                                    GROUP BY user_id
+                                    HAVING
+                                        COUNT(user_id) > 1 ) tempA
+                                """);
         //设置回传对象
         sqlNewUserAgainNum.setCallback(Sqls.callback.longValue());
         //执行sql
@@ -1092,14 +1231,14 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
         //创角人数
         Sql sqlRoleNum = Sqls.create(
                 """
-                SELECT
-                    COUNT(DISTINCT role_user_id) as role_num
-                FROM
-                    dw_create_role_detail
-                """ + criRoleNum +
-                """
-                AND NOT user_agent_id = 0
-                """);
+                        SELECT
+                            COUNT(DISTINCT role_user_id) as role_num
+                        FROM
+                            dw_create_role_detail
+                        """ + criRoleNum +
+                        """
+                                AND NOT user_agent_id = 0
+                                """);
         //设置回传对象
         sqlRoleNum.setCallback(Sqls.callback.longValue());
         //执行sql
@@ -1590,26 +1729,26 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
         Sql dayNSql;
         if (dto.getGameDimension() == 1L) {
             dayNSql = Sqls.create("""
-                SELECT
-                    dt,
-                    game_id,
-                    pitcher_id,
-                    source_system,
-                    dayN
-                FROM
-                    game_ads.ads_pitcher_game_dayn
-                """ + cri);
+                    SELECT
+                        dt,
+                        game_id,
+                        pitcher_id,
+                        source_system,
+                        dayN
+                    FROM
+                        game_ads.ads_pitcher_game_dayn
+                    """ + cri);
         } else {
             dayNSql = Sqls.create("""
-                SELECT
-                    dt,
-                    parent_game_id as game_id,
-                    pitcher_id,
-                    source_system,
-                    dayN
-                FROM
-                    game_ads_parent.ads_pitcher_game_dayn_parent
-                """ + cri);
+                    SELECT
+                        dt,
+                        parent_game_id as game_id,
+                        pitcher_id,
+                        source_system,
+                        dayN
+                    FROM
+                        game_ads_parent.ads_pitcher_game_dayn_parent
+                    """ + cri);
         }
 
         //设置回传
@@ -1731,26 +1870,26 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
         Sql dayNSql;
         if (dto.getGameDimension() == 1L) {
             dayNSql = Sqls.create("""
-                SELECT
-                    dt,
-                    game_id,
-                    pitcher_id,
-                    source_system,
-                    dayN
-                FROM
-                    game_ads.ads_pitcher_game_dayn
-                """ + cri);
+                    SELECT
+                        dt,
+                        game_id,
+                        pitcher_id,
+                        source_system,
+                        dayN
+                    FROM
+                        game_ads.ads_pitcher_game_dayn
+                    """ + cri);
         } else {
             dayNSql = Sqls.create("""
-                SELECT
-                    dt,
-                    parent_game_id as game_id,
-                    pitcher_id,
-                    source_system,
-                    dayN
-                FROM
-                    game_ads_parent.ads_pitcher_game_dayn_parent
-                """ + cri);
+                    SELECT
+                        dt,
+                        parent_game_id as game_id,
+                        pitcher_id,
+                        source_system,
+                        dayN
+                    FROM
+                        game_ads_parent.ads_pitcher_game_dayn_parent
+                    """ + cri);
         }
 
         //设置回传
@@ -2541,9 +2680,9 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
                     ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total,
                 """ + getPitcherGameDataDayTotalDayNsql() +
                 """
-                FROM
-                    game_ads.ads_game_pitcher_day
-                """;
+                        FROM
+                            game_ads.ads_game_pitcher_day
+                        """;
     }
 
     /**
@@ -2649,9 +2788,9 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
                     ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total,
                 """ + getPitcherGameDataDayTotalDayNsql() +
                 """
-                FROM
-                    game_ads_parent.ads_game_pitcher_day_parent
-                """;
+                        FROM
+                            game_ads_parent.ads_game_pitcher_day_parent
+                        """;
     }
 
     /**
@@ -2753,8 +2892,8 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
                     ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total,
                 """ + getPitcherGameDataDayTotalDayNsql() +
                 """
-                FROM 
-                """ + tableName;
+                        FROM 
+                        """ + tableName;
     }
 
     /**
@@ -2856,8 +2995,8 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
                     ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total,
                 """ + getPitcherGameDataDayTotalDayNsql() +
                 """
-                FROM 
-                """ + tableName;
+                        FROM 
+                        """ + tableName;
     }
 
     /**
@@ -3135,87 +3274,87 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
                     	ROUND(IF(SUM(hundred_user_num) > 0 , SUM(cost) / SUM(hundred_user_num), 0), 2) as hundred_user_num_cost,
                 """ + pitcherDataTotalRoiSql() +
                 """
-                        ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                            SUM(da60) / SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi60,
-                        ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                            SUM(da90) / SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi90,
-                        ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                            SUM(m6) / SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi180,
-                        ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)) > 0 ,
-                            SUM(m12) / SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)), 0), 4) as roi1yaer,
-                        ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total
-                    FROM
-                        game_ads_parent.ads_pitcher_day_parent
-                """ + criA +
+                                ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                    SUM(da60) / SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi60,
+                                ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                    SUM(da90) / SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi90,
+                                ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                    SUM(m6) / SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi180,
+                                ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)) > 0 ,
+                                    SUM(m12) / SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)), 0), 4) as roi1yaer,
+                                ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total
+                            FROM
+                                game_ads_parent.ads_pitcher_day_parent
+                        """ + criA +
                 """
-                ) a
-                LEFT JOIN (
-                    SELECT
-                        pitcher_id as b_pitcher_id,
-                        IFNULL(SUM(amount), 0) as amount,
-                        IFNULL(SUM(amount_count), 0) as amount_count,
-                        ROUND(IF(SUM(amount_count) > 0 , SUM(amount) / SUM(amount_count), 0), 2) as paper_avg
-                    FROM
-                        game_ads_parent.ads_pitcher_day_parent
-                """ + criB +
+                        ) a
+                        LEFT JOIN (
+                            SELECT
+                                pitcher_id as b_pitcher_id,
+                                IFNULL(SUM(amount), 0) as amount,
+                                IFNULL(SUM(amount_count), 0) as amount_count,
+                                ROUND(IF(SUM(amount_count) > 0 , SUM(amount) / SUM(amount_count), 0), 2) as paper_avg
+                            FROM
+                                game_ads_parent.ads_pitcher_day_parent
+                        """ + criB +
                 """
-                ) b ON a.pitcher_id = b.b_pitcher_id
-                LEFT JOIN (
-                    SELECT
-                        pitcher_id as c_pitcher_id,
-                        IFNULL(COUNT(DISTINCT user_id), 0) amount_num
-                    FROM
-                        game_ads.ads_information
-                """ + criAmount +
+                        ) b ON a.pitcher_id = b.b_pitcher_id
+                        LEFT JOIN (
+                            SELECT
+                                pitcher_id as c_pitcher_id,
+                                IFNULL(COUNT(DISTINCT user_id), 0) amount_num
+                            FROM
+                                game_ads.ads_information
+                        """ + criAmount +
                 """
-                        AND NOT agent_id = 0
-                    GROUP BY pitcher_id
-                ) c ON a.pitcher_id = c.c_pitcher_id
-                LEFT JOIN (
-                    SELECT
-                        pitcher_id as d_pitcher_id,
-                        COUNT(DISTINCT user_id) new_user_amount_num
-                    FROM
-                        game_ads.ads_information
-                """ + criNewUser +
+                                AND NOT agent_id = 0
+                            GROUP BY pitcher_id
+                        ) c ON a.pitcher_id = c.c_pitcher_id
+                        LEFT JOIN (
+                            SELECT
+                                pitcher_id as d_pitcher_id,
+                                COUNT(DISTINCT user_id) new_user_amount_num
+                            FROM
+                                game_ads.ads_information
+                        """ + criNewUser +
                 """
-                        AND NOT agent_id = 0
-                    GROUP BY
-                        pitcher_id
-                ) d ON a.pitcher_id = d.d_pitcher_id
-                LEFT JOIN (
-                    SELECT
-                        pitcher_id as e_pitcher_id,
-                        COUNT(tempA.num) as new_user_order_again
-                    FROM (
-                        SELECT
-                            pitcher_id,
-                            COUNT(user_id) num
-                        FROM
-                            game_ads.ads_information
-                """ + criNewUser +
+                                AND NOT agent_id = 0
+                            GROUP BY
+                                pitcher_id
+                        ) d ON a.pitcher_id = d.d_pitcher_id
+                        LEFT JOIN (
+                            SELECT
+                                pitcher_id as e_pitcher_id,
+                                COUNT(tempA.num) as new_user_order_again
+                            FROM (
+                                SELECT
+                                    pitcher_id,
+                                    COUNT(user_id) num
+                                FROM
+                                    game_ads.ads_information
+                        """ + criNewUser +
                 """
-                            AND NOT agent_id = 0
-                        GROUP BY
-                            user_id,
-                            pitcher_id
-                        HAVING
-                            COUNT(user_id) > 1 ) tempA
-                    GROUP BY
-                        tempA.pitcher_id
-                ) e ON a.pitcher_id = e.e_pitcher_id
-                LEFT JOIN (
-                    SELECT
-                        user_zx_pitcher_id as f_pitcher_id,
-                        COUNT(DISTINCT role_user_id) as role_num
-                    FROM
-                        dw_create_role_detail
-                """ + criRoleNum +
+                                    AND NOT agent_id = 0
+                                GROUP BY
+                                    user_id,
+                                    pitcher_id
+                                HAVING
+                                    COUNT(user_id) > 1 ) tempA
+                            GROUP BY
+                                tempA.pitcher_id
+                        ) e ON a.pitcher_id = e.e_pitcher_id
+                        LEFT JOIN (
+                            SELECT
+                                user_zx_pitcher_id as f_pitcher_id,
+                                COUNT(DISTINCT role_user_id) as role_num
+                            FROM
+                                dw_create_role_detail
+                        """ + criRoleNum +
                 """
-                        AND NOT user_agent_id = 0
-                    GROUP BY user_zx_pitcher_id
-                ) f ON a.pitcher_id = f.f_pitcher_id
-                """;
+                                AND NOT user_agent_id = 0
+                            GROUP BY user_zx_pitcher_id
+                        ) f ON a.pitcher_id = f.f_pitcher_id
+                        """;
     }
 
     /**
@@ -3483,93 +3622,93 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
                 		ROUND(IF(SUM(hundred_user_num) > 0 , SUM(cost) / SUM(hundred_user_num), 0), 2) as hundred_user_num_cost,
                 """ + pitcherDataTotalRoiSql() +
                 """
-                        ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                              SUM(da60) / SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi60,
-                        ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                              SUM(da90) / SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi90,
-                        ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                              SUM(m6) / SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi180,
-                        ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)) > 0 ,
-                              SUM(m12) / SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)), 0), 4) as roi1yaer,
-                        ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total
-                    FROM
-                        game_ads_parent.ads_game_pitcher_day_parent
-                """ + criA +
-                """
-                ) a
-                LEFT JOIN (
-                    SELECT
-                        pitcher_id as b_pitcher_id,
-                        parent_game_id as b_game_id,
-                        IFNULL(SUM(amount), 0) as amount,
-                        IFNULL(SUM(amount_count), 0) as amount_count,
-                        ROUND(IF(SUM(amount_count) > 0 , SUM(amount) / SUM(amount_count), 0), 2) as paper_avg
-                    FROM
-                        game_ads_parent.ads_game_pitcher_day_parent
-                """ + criB +
+                                ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                      SUM(da60) / SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi60,
+                                ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                      SUM(da90) / SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi90,
+                                ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                      SUM(m6) / SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi180,
+                                ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)) > 0 ,
+                                      SUM(m12) / SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)), 0), 4) as roi1yaer,
+                                ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total
+                            FROM
+                                game_ads_parent.ads_game_pitcher_day_parent
+                        """ + criA +
                 """
-                ) b ON a.pitcher_id = b.b_pitcher_id AND a.game_id = b.b_game_id
-                LEFT JOIN (
-                    SELECT
-                        pitcher_id as c_pitcher_id,
-                        parent_game_id as c_game_id,
-                        IFNULL(COUNT(DISTINCT user_id), 0) amount_num
-                    FROM
-                        game_ads.ads_information
-                """ + criAmount +
+                        ) a
+                        LEFT JOIN (
+                            SELECT
+                                pitcher_id as b_pitcher_id,
+                                parent_game_id as b_game_id,
+                                IFNULL(SUM(amount), 0) as amount,
+                                IFNULL(SUM(amount_count), 0) as amount_count,
+                                ROUND(IF(SUM(amount_count) > 0 , SUM(amount) / SUM(amount_count), 0), 2) as paper_avg
+                            FROM
+                                game_ads_parent.ads_game_pitcher_day_parent
+                        """ + criB +
                 """
-                        AND NOT agent_id = 0
-                    GROUP BY pitcher_id , parent_game_id
-                ) c ON a.pitcher_id = c.c_pitcher_id AND a.game_id = c.c_game_id
-                LEFT JOIN (
-                    SELECT
-                        pitcher_id as d_pitcher_id,
-                        parent_game_id as d_game_id,
-                        COUNT(DISTINCT user_id) new_user_amount_num
-                    FROM
-                        game_ads.ads_information
-                """ + criNewUser +
+                        ) b ON a.pitcher_id = b.b_pitcher_id AND a.game_id = b.b_game_id
+                        LEFT JOIN (
+                            SELECT
+                                pitcher_id as c_pitcher_id,
+                                parent_game_id as c_game_id,
+                                IFNULL(COUNT(DISTINCT user_id), 0) amount_num
+                            FROM
+                                game_ads.ads_information
+                        """ + criAmount +
                 """
-                    AND NOT agent_id = 0
-                    GROUP BY
-                        pitcher_id , parent_game_id
-                ) d ON a.pitcher_id = d.d_pitcher_id AND a.game_id = d.d_game_id
-                LEFT JOIN (
-                    SELECT
-                        parent_game_id as e_game_id,
-                        pitcher_id as e_pitcher_id,
-                        COUNT(tempA.num) as new_user_order_again
-                    FROM (
-                        SELECT
-                            parent_game_id,
-                            pitcher_id,
-                            COUNT(user_id) num
-                        FROM
-                            game_ads.ads_information
-                """ + criNewUser +
+                                AND NOT agent_id = 0
+                            GROUP BY pitcher_id , parent_game_id
+                        ) c ON a.pitcher_id = c.c_pitcher_id AND a.game_id = c.c_game_id
+                        LEFT JOIN (
+                            SELECT
+                                pitcher_id as d_pitcher_id,
+                                parent_game_id as d_game_id,
+                                COUNT(DISTINCT user_id) new_user_amount_num
+                            FROM
+                                game_ads.ads_information
+                        """ + criNewUser +
                 """
                             AND NOT agent_id = 0
-                        GROUP BY
-                            user_id,
-                            pitcher_id,
-                            parent_game_id
-                        HAVING
-                            COUNT(user_id) > 1 ) tempA
-                    GROUP BY tempA.parent_game_id, tempA.pitcher_id
-                ) e ON a.game_id = e.e_game_id AND a.pitcher_id = e.e_pitcher_id
-                LEFT JOIN (
-                    SELECT
-                        parent_game_id as f_game_id,
-                        user_zx_pitcher_id as f_pitcher_id,
-                        COUNT(DISTINCT role_user_id) as role_num
-                    FROM
-                        dw_create_role_detail
-                """ + criRoleNum +
+                            GROUP BY
+                                pitcher_id , parent_game_id
+                        ) d ON a.pitcher_id = d.d_pitcher_id AND a.game_id = d.d_game_id
+                        LEFT JOIN (
+                            SELECT
+                                parent_game_id as e_game_id,
+                                pitcher_id as e_pitcher_id,
+                                COUNT(tempA.num) as new_user_order_again
+                            FROM (
+                                SELECT
+                                    parent_game_id,
+                                    pitcher_id,
+                                    COUNT(user_id) num
+                                FROM
+                                    game_ads.ads_information
+                        """ + criNewUser +
                 """
-                        AND NOT user_agent_id = 0
-                    GROUP BY parent_game_id, user_zx_pitcher_id
-                ) f ON a.game_id = f.f_game_id AND a.pitcher_id = f.f_pitcher_id
-                """;
+                                    AND NOT agent_id = 0
+                                GROUP BY
+                                    user_id,
+                                    pitcher_id,
+                                    parent_game_id
+                                HAVING
+                                    COUNT(user_id) > 1 ) tempA
+                            GROUP BY tempA.parent_game_id, tempA.pitcher_id
+                        ) e ON a.game_id = e.e_game_id AND a.pitcher_id = e.e_pitcher_id
+                        LEFT JOIN (
+                            SELECT
+                                parent_game_id as f_game_id,
+                                user_zx_pitcher_id as f_pitcher_id,
+                                COUNT(DISTINCT role_user_id) as role_num
+                            FROM
+                                dw_create_role_detail
+                        """ + criRoleNum +
+                """
+                                AND NOT user_agent_id = 0
+                            GROUP BY parent_game_id, user_zx_pitcher_id
+                        ) f ON a.game_id = f.f_game_id AND a.pitcher_id = f.f_pitcher_id
+                        """;
     }
 
     /**
@@ -3623,17 +3762,17 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
                     ROUND(IF(SUM(hundred_user_num) > 0 , SUM(cost) / SUM(hundred_user_num), 0), 2) as hundred_user_num_cost,
                 """ + pitcherDataTotalRoiSql() +
                 """
-                    ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                          SUM(da60) / SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi60,
-                    ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                          SUM(da90) / SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi90,
-                    ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                          SUM(m6) / SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi180,
-                    ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)) > 0 ,
-                          SUM(m12) / SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)), 0), 4) as roi1yaer,
-                    ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total
-                FROM 
-                """ + tableName;
+                            ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                  SUM(da60) / SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi60,
+                            ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                  SUM(da90) / SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi90,
+                            ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                  SUM(m6) / SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi180,
+                            ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)) > 0 ,
+                                  SUM(m12) / SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)), 0), 4) as roi1yaer,
+                            ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total
+                        FROM 
+                        """ + tableName;
     }
 
     /**
@@ -3687,17 +3826,17 @@ public class PitcherDataServiceImpl implements IPitcherDataService {
                     ROUND(IF(SUM(hundred_user_num) > 0 , SUM(cost) / SUM(hundred_user_num), 0), 2) as hundred_user_num_cost,
                 """ + pitcherDataTotalRoiSql() +
                 """
-                    ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                          SUM(da60) / SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi60,
-                    ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                          SUM(da90) / SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi90,
-                    ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)) > 0 ,
-                          SUM(m6) / SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi180,
-                    ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)) > 0 ,
-                          SUM(m12) / SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)), 0), 4) as roi1yaer,
-                    ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total
-                FROM 
-                """ + tableName;
+                            ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                  SUM(da60) / SUM(IF(DATE_ADD(dt, INTERVAL 59 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi60,
+                            ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                  SUM(da90) / SUM(IF(DATE_ADD(dt, INTERVAL 89 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi90,
+                            ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)) > 0 ,
+                                  SUM(m6) / SUM(IF(DATE_ADD(dt, INTERVAL 179 day) <= LocalDate.now(), cost, 0)), 0), 4) as roi180,
+                            ROUND(IF(SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)) > 0 ,
+                                  SUM(m12) / SUM(IF(DATE_ADD(dt, INTERVAL 1 year) <= LocalDate.now(), cost, 0)), 0), 4) as roi1yaer,
+                            ROUND(IF(SUM(cost) > 0 , SUM(total) / SUM(cost), 0), 4) as roi_total
+                        FROM 
+                        """ + tableName;
     }
 
 

+ 68 - 6
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/PlayerDataServiceImpl.java

@@ -161,6 +161,18 @@ public class PlayerDataServiceImpl implements IPlayerDataService {
             //创角最大数
             cri.where().andLTE("role_count", dto.getRoleCountMax());
         }
+        if (dto.getUserLastRegTime() != null) {
+            //玩家最新染色时间
+            cri.where().andEquals("DATE(user_last_reg_time)", dto.getUserLastRegTime());
+        }
+        if (dto.getUserLastPitcherId() != null) {
+            //玩家最新染色归因投手
+            cri.where().andEquals("user_last_pitcher_id", dto.getUserLastPitcherId());
+        }
+        if (dto.getUserLastRegAgentId() != null) {
+            //玩家最新染色归因投手
+            cri.where().andEquals("user_last_reg_agent_id", dto.getUserLastRegAgentId());
+        }
 
         //设置pager
         Pager pager = dao.createPager(dto.getPageNum(), dto.getPageSize());
@@ -641,7 +653,12 @@ public class PlayerDataServiceImpl implements IPlayerDataService {
                         a.relation_user_id as relation_user_id ,
                         a.relation_create_time as relation_create_time ,
                         a.`authentication` as auth,
-                        TIMESTAMPDIFF(SECOND , a.create_time, b.last_recharge_time) as reg_pay_time_diff
+                        TIMESTAMPDIFF(SECOND , a.create_time, b.last_recharge_time) as reg_pay_time_diff,
+                        k.create_time as user_last_reg_time,
+                        k.agent_id as user_last_reg_agent_id,
+                        IF(k.agent_id = 0, '自然量', l.agent_name) as user_last_agent_name ,
+                        l.put_user_id as user_last_pitcher_id,
+                        IFNULL(m.zx_pitcher_name, '自然量') as user_last_pitcher_name
                     FROM dm_game_order.t_game_user a
                     left join (
                         select
@@ -705,7 +722,26 @@ public class PlayerDataServiceImpl implements IPlayerDataService {
                     		ROW_NUMBER()over(partition by user_id , source_system order by create_time desc) as num
                     	FROM dm_game_order.t_user_login_log
                     ) j ON a.id = j.user_id AND a.source_system = j.source_system AND j.num = 1
-                ) k
+                    LEFT JOIN (
+                    	select
+                    		id,
+                    		source_system,
+                    		create_time,
+                    		agent_id,
+                    		ROW_NUMBER() OVER(PARTITION BY id,source_system order by create_time desc) as num
+                    	FROM dm_game_order.t_game_user_burst
+                    ) k on a.id = k.id AND a.source_system = k.source_system AND k.num = 1
+                    LEFT JOIN (
+                        SELECT
+                            -- 渠道名称、投手id
+                            source_system,
+                            id,
+                            agent_name,
+                            pitcher_id as put_user_id
+                        FROM dm_game_order.t_pitcher_agent
+                    ) l on k.source_system = l.source_system AND k.agent_id = l.id
+                    left join dm_game_order.t_pitcher_map m on l.put_user_id = m.zx_pitcher_id and l.source_system = m.source_system
+                ) a
                 """;
     }
 
@@ -749,7 +785,12 @@ public class PlayerDataServiceImpl implements IPlayerDataService {
                         a.relation_user_id as relation_user_id ,
                         a.relation_create_time as relation_create_time ,
                         a.`authentication` as auth,
-                        TIMESTAMPDIFF(SECOND , a.create_time, b.last_recharge_time) as reg_pay_time_diff
+                        TIMESTAMPDIFF(SECOND , a.create_time, b.last_recharge_time) as reg_pay_time_diff,
+                        k.create_time as user_last_reg_time,
+                        k.agent_id as user_last_reg_agent_id,
+                        IF(k.agent_id = 0, '自然量', l.agent_name) as user_last_agent_name ,
+                        l.put_user_id as user_last_pitcher_id,
+                        IFNULL(m.zx_pitcher_name, '自然量') as user_last_pitcher_name
                     FROM dm_game_order.t_game_user a
                     left join (
                         select
@@ -813,7 +854,26 @@ public class PlayerDataServiceImpl implements IPlayerDataService {
                     		ROW_NUMBER()over(partition by user_id , source_system order by create_time desc) as num
                     	FROM dm_game_order.t_user_login_log
                     ) j ON a.id = j.user_id AND a.source_system = j.source_system AND j.num = 1
-                ) k
+                    LEFT JOIN (
+                    	select
+                    		id,
+                    		source_system,
+                    		create_time,
+                    		agent_id,
+                    		ROW_NUMBER() OVER(PARTITION BY id,source_system order by create_time desc) as num
+                    	FROM dm_game_order.t_game_user_burst
+                    ) k on a.id = k.id AND a.source_system = k.source_system AND k.num = 1
+                    LEFT JOIN (
+                        SELECT
+                            -- 渠道名称、投手id
+                            source_system,
+                            id,
+                            agent_name,
+                            pitcher_id as put_user_id
+                        FROM dm_game_order.t_pitcher_agent
+                    ) l on k.source_system = l.source_system AND k.agent_id = l.id
+                    left join dm_game_order.t_pitcher_map m on l.put_user_id = m.zx_pitcher_id and l.source_system = m.source_system
+                ) a
                 """;
     }
 
@@ -865,7 +925,8 @@ public class PlayerDataServiceImpl implements IPlayerDataService {
                 		j.amount as recharge_money, -- 角色充值金额
                 		j.amount_count as recharge_count -- 角色充值次数
                 	FROM dm_game_order.t_game_user_role a
-                	LEFT JOIN dm_game_order.t_game_user b on a.source_system = b.source_system AND a.user_id = b.id
+                	LEFT JOIN dm_game_order.t_game_user_burst b
+                	on a.source_system = b.source_system AND a.user_id = b.id AND a.create_time >= b.create_time AND a.create_time < b.end_time
                 	LEFT JOIN dm_game_order.t_pitcher_agent c on b.source_system = c.source_system AND b.agent_id = c.id
                 	LEFT JOIN dm_game_order.t_pitcher_map d on c.source_system = d.source_system AND c.pitcher_id = d.zx_pitcher_id
                 	LEFT JOIN dm_game_order.t_game e on a.source_system = e.source_system AND a.game_id = e.id
@@ -991,7 +1052,8 @@ public class PlayerDataServiceImpl implements IPlayerDataService {
                 		j.amount as recharge_money, -- 角色充值金额
                 		j.amount_count as recharge_count -- 角色充值次数
                 	FROM dm_game_order.t_game_user_role a
-                	LEFT JOIN dm_game_order.t_game_user b on a.source_system = b.source_system AND a.user_id = b.id
+                	LEFT JOIN dm_game_order.t_game_user_burst b
+                	on a.source_system = b.source_system AND a.user_id = b.id AND a.create_time >= b.create_time AND a.create_time < b.end_time
                 	LEFT JOIN dm_game_order.t_pitcher_agent c on b.source_system = c.source_system AND b.agent_id = c.id
                 	LEFT JOIN dm_game_order.t_pitcher_map d on c.source_system = d.source_system AND c.pitcher_id = d.zx_pitcher_id
                 	LEFT JOIN dm_game_order.t_game e on a.source_system = e.source_system AND a.game_id = e.id

+ 74 - 60
game-data/game-data-serve/src/main/java/com/zanxiang/game/data/serve/service/impl/RoleManageServiceImpl.java

@@ -898,9 +898,9 @@ public class RoleManageServiceImpl implements IRoleManageService {
                 	    i.user_create_time as user_create_time, -- 玩家注册时间
                 	    i.user_reg_game_id as user_reg_game_id, -- 玩家注册游戏id
                 	    i.user_reg_game_name as user_reg_game_name, -- 玩家注册游戏名
-                	    i.agent_id as agent_id, -- 玩家注册渠道id
-                	    IF(i.agent_id = 0 , '自然量', i.agent_name) as agent_name, -- 玩家注册渠道名
-                	    i.put_user_id as put_user_id, -- 投手id
+                	    x.agent_id as agent_id, -- 玩家注册渠道id
+                	    IF(x.agent_id = 0 , '自然量', y.agent_name) as agent_name, -- 玩家注册渠道名
+                	    y.put_user_id as put_user_id, -- 投手id
                 	    i.user_active_time as user_active_time, -- 玩家最近活跃时间
                 	    TIMESTAMPDIFF(SECOND, i.user_active_time, NOW()) as user_active_until_now, -- 玩家最近活跃距今(秒)
                 	    i.user_last_recharge_game_id as user_last_recharge_game_id, -- 玩家最近充值游戏id
@@ -1068,9 +1068,6 @@ public class RoleManageServiceImpl implements IRoleManageService {
                             i.user_create_time as user_create_time,
                             i.user_reg_game_id as user_reg_game_id,
                             k.game_name as user_reg_game_name,
-                            i.agent_id as agent_id,
-                            IF(i.agent_id = 0 , '自然量', j.agent_name) as agent_name,
-                            j.pitcher_id as put_user_id,
                             l.update_time as user_active_time,
                             m.user_last_recharge_game_id as user_last_recharge_game_id,
                             n.game_name as user_last_recharge_game_name,
@@ -1091,20 +1088,10 @@ public class RoleManageServiceImpl implements IRoleManageService {
                                     reg_email ,
                                     create_time as user_create_time,
                                     game_id as user_reg_game_id,
-                                    agent_id ,
                                     ROW_NUMBER()over(partition by association_user_id, source_system order by create_time desc, id asc) as num
                                 FROM dm_game_order.t_game_user
                             ) z WHERE z.num = 1
                         ) i
-                        LEFT JOIN (
-                            SELECT
-                                -- 渠道名称、投手id
-                                source_system,
-                                id,
-                                agent_name,
-                                pitcher_id
-                            FROM dm_game_order.t_pitcher_agent
-                        ) j ON i.agent_id = j.id AND i.source_system  = j.source_system
                         LEFT JOIN (
                             SELECT
                                 -- 玩家注册游戏名
@@ -1225,6 +1212,26 @@ public class RoleManageServiceImpl implements IRoleManageService {
                 	) w on d.source_system = w.source_system AND IFNULL(d.parent_id, a.game_id) = w.parent_game_id
                 	AND IFNULL(d.super_game_id, a.game_id) = w.super_game_id
                 	AND c.amount >= w.recharge_money_min AND c.amount < recharge_money_max
+                	LEFT JOIN (
+                		SELECT
+                			source_system,
+                			id,
+                			create_time,
+                			end_time,
+                			agent_id,
+                			association_user_id
+                		FROM dm_game_order.t_game_user_burst
+                	) x on a.source_system = x.source_system AND a.association_user_id = x.id
+                	AND a.create_time >= x.create_time AND a.create_time < x.end_time
+                	LEFT JOIN (
+                        SELECT
+                            -- 渠道名称、投手id
+                            source_system,
+                            id,
+                            agent_name,
+                            pitcher_id as put_user_id
+                        FROM dm_game_order.t_pitcher_agent
+                    ) y ON x.agent_id = y.id AND x.source_system = y.source_system
                 ) a
                 """ + criA;
     }
@@ -1291,9 +1298,9 @@ public class RoleManageServiceImpl implements IRoleManageService {
                         i.user_create_time as user_create_time, -- 玩家注册时间
                         i.user_reg_game_id as user_reg_game_id, -- 玩家注册游戏id
                         i.user_reg_game_name as user_reg_game_name, -- 玩家注册游戏名
-                        i.agent_id as agent_id, -- 玩家注册渠道id
-                        IF(i.agent_id = 0 , '自然量', i.agent_name) as agent_name, -- 玩家注册渠道名
-                        i.put_user_id as put_user_id, -- 投手id
+                        x.agent_id as agent_id, -- 玩家注册渠道id
+                        IF(x.agent_id = 0 , '自然量', y.agent_name) as agent_name, -- 玩家注册渠道名
+                        y.put_user_id as put_user_id, -- 投手id
                         i.user_active_time as user_active_time, -- 玩家最近活跃时间
                         TIMESTAMPDIFF(SECOND, i.user_active_time, NOW()) as user_active_until_now, -- 玩家最近活跃距今(秒)
                         i.user_last_recharge_game_id as user_last_recharge_game_id, -- 玩家最近充值游戏id
@@ -1461,9 +1468,6 @@ public class RoleManageServiceImpl implements IRoleManageService {
                             i.user_create_time as user_create_time,
                             i.user_reg_game_id as user_reg_game_id,
                             k.game_name as user_reg_game_name,
-                            i.agent_id as agent_id,
-                            IF(i.agent_id = 0 , '自然量', j.agent_name) as agent_name,
-                            j.pitcher_id as put_user_id,
                             l.update_time as user_active_time,
                             m.user_last_recharge_game_id as user_last_recharge_game_id,
                             n.game_name as user_last_recharge_game_name,
@@ -1484,20 +1488,10 @@ public class RoleManageServiceImpl implements IRoleManageService {
                                     reg_email ,
                                     create_time as user_create_time,
                                     game_id as user_reg_game_id,
-                                    agent_id ,
                                     ROW_NUMBER()over(partition by association_user_id, source_system order by create_time desc, id asc) as num
                                 FROM dm_game_order.t_game_user
                             ) z WHERE z.num = 1
                         ) i
-                        LEFT JOIN (
-                            SELECT
-                                -- 渠道名称、投手id
-                                source_system,
-                                id,
-                                agent_name,
-                                pitcher_id
-                            FROM dm_game_order.t_pitcher_agent
-                        ) j ON i.agent_id = j.id AND i.source_system  = j.source_system
                         LEFT JOIN (
                             SELECT
                                 -- 玩家注册游戏名
@@ -1508,19 +1502,19 @@ public class RoleManageServiceImpl implements IRoleManageService {
                         ) k ON i.user_reg_game_id = k.id AND i.source_system = k.source_system
                         LEFT JOIN (
                             -- 玩家最近活跃时间
-                			SELECT
-                			    association_user_id,
-                			    b.source_system,
-                			    b.active_time as update_time,
-                			    ROW_NUMBER()over(partition by association_user_id, b.source_system order by b.active_time desc) as num
-                			FROM dm_game_order.t_game_user a
-                			LEFT JOIN (
-                			    SELECT
-                			    	source_system ,
-                			    	user_id,
-                			    	active_time
-                			    FROM game_dw.dw_active_log
-                			) b ON a.source_system = b.source_system AND a.id = b.user_id
+                            SELECT
+                                association_user_id,
+                                b.source_system,
+                                b.active_time as update_time,
+                                ROW_NUMBER()over(partition by association_user_id, b.source_system order by b.active_time desc) as num
+                            FROM dm_game_order.t_game_user a
+                            LEFT JOIN (
+                                SELECT
+                                    source_system ,
+                                    user_id,
+                                    active_time
+                                FROM game_dw.dw_active_log
+                            ) b ON a.source_system = b.source_system AND a.id = b.user_id
                         ) l ON i.association_user_id = l.association_user_id AND i.source_system = l.source_system AND l.num = 1
                         LEFT JOIN (
                             -- 玩家最近充值游戏、玩家最近充值时间
@@ -1603,21 +1597,41 @@ public class RoleManageServiceImpl implements IRoleManageService {
                         FROM dm_game_order.t_game_server_merge
                         WHERE is_delete = 0
                     ) t on a.source_system = t.source_system AND a.server_id = t.server_id AND d.super_game_id = t.game_id
-                	LEFT JOIN (
-                		SELECT
-                			-- vip等级
-                			source_system ,
-                			super_game_id ,
-                			parent_game_id ,
-                			recharge_money_min ,
-                			recharge_money_max ,
-                			vip_level ,
-                			is_delete
-                		FROM dm_game_order.t_game_vip
-                		WHERE is_delete = 0
-                	) w on d.source_system = w.source_system AND IFNULL(d.parent_id, a.game_id) = w.parent_game_id
-                	AND IFNULL(d.super_game_id, a.game_id) = w.super_game_id
-                	AND c.amount >= w.recharge_money_min AND c.amount < recharge_money_max
+                    LEFT JOIN (
+                        SELECT
+                            -- vip等级
+                            source_system ,
+                            super_game_id ,
+                            parent_game_id ,
+                            recharge_money_min ,
+                            recharge_money_max ,
+                            vip_level ,
+                            is_delete
+                        FROM dm_game_order.t_game_vip
+                        WHERE is_delete = 0
+                    ) w on d.source_system = w.source_system AND IFNULL(d.parent_id, a.game_id) = w.parent_game_id
+                    AND IFNULL(d.super_game_id, a.game_id) = w.super_game_id
+                    AND c.amount >= w.recharge_money_min AND c.amount < recharge_money_max
+                    LEFT JOIN (
+                        SELECT
+                            source_system,
+                            id,
+                            create_time,
+                            end_time,
+                            agent_id,
+                            association_user_id
+                        FROM dm_game_order.t_game_user_burst
+                    ) x on a.source_system = x.source_system AND a.association_user_id = x.id
+                    AND a.create_time >= x.create_time AND a.create_time < x.end_time
+                    LEFT JOIN (
+                        SELECT
+                            -- 渠道名称、投手id
+                            source_system,
+                            id,
+                            agent_name,
+                            pitcher_id as put_user_id
+                        FROM dm_game_order.t_pitcher_agent
+                    ) y ON x.agent_id = y.id AND x.source_system = y.source_system
                 ) a
                 """ + criA;
     }

+ 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 - 1
game-module/game-module-manage/src/main/java/com/zanxiang/game/module/manage/ManageApplication.java

@@ -23,7 +23,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客服系统代码合并> ( ´・・)ノ(._.`) \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;
+    }
 }

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác