diff --git a/src/main/java/com/tailbet/job/GameJob.java b/src/main/java/com/tailbet/job/GameJob.java index 26fe71b..4d05978 100644 --- a/src/main/java/com/tailbet/job/GameJob.java +++ b/src/main/java/com/tailbet/job/GameJob.java @@ -44,6 +44,12 @@ public class GameJob { // 局状态监控(使用内存缓存,实际生产应使用Redis) private final Map roundTasks = new ConcurrentHashMap<>(); + // 延迟任务队列 + private final Map pendingTasks = new ConcurrentHashMap<>(); + + // 任务ID生成器 + private java.util.concurrent.atomic.AtomicLong taskIdGenerator = new java.util.concurrent.atomic.AtomicLong(1); + /** * 检查红包是否需要托领取(每3秒执行) */ @@ -131,20 +137,65 @@ public class GameJob { /** * 安排开始发红包 */ - public void scheduleStartRp(Long roundId, Long groupId, int delaySeconds) { - RoundTask task = new RoundTask(); - task.roundId = roundId; - task.groupId = groupId; - task.startRpTime = LocalDateTime.now().plusSeconds(delaySeconds); - roundTasks.put(roundId, task); + public String scheduleStartRp(Long roundId, Long groupId, int delaySeconds) { + String taskId = "start_rp_" + taskIdGenerator.getAndIncrement(); + PendingTask task = new PendingTask(); + task.setTaskId(taskId); + task.setType("start_rp"); + task.setRoundId(roundId); + task.setGroupId(groupId); + task.setExecuteTime(LocalDateTime.now().plusSeconds(delaySeconds)); + pendingTasks.put(taskId, task); + log.info("安排{}秒后开始发红包: roundId={}", delaySeconds, roundId); + return taskId; } /** * 安排自动结算 */ - public void scheduleSettle(Long roundId, Long groupId, int digit, int delaySeconds) { - // TODO: 使用延迟队列或定时任务实现 - log.info("安排{}秒后结算 roundId={}, digit={}", delaySeconds, roundId, digit); + public String scheduleSettle(Long roundId, Long groupId, int digit, int delaySeconds) { + String taskId = "settle_" + taskIdGenerator.getAndIncrement(); + PendingTask task = new PendingTask(); + task.setTaskId(taskId); + task.setType("settle"); + task.setRoundId(roundId); + task.setGroupId(groupId); + task.setDigit(digit); + task.setExecuteTime(LocalDateTime.now().plusSeconds(delaySeconds)); + pendingTasks.put(taskId, task); + log.info("安排{}秒后结算: taskId={}, roundId={}, digit={}", delaySeconds, taskId, roundId, digit); + return taskId; + } + + /** + * 安排自动下一期 + */ + public String scheduleAutoNextTask(Long groupId, int delaySeconds) { + String taskId = "auto_next_" + taskIdGenerator.getAndIncrement(); + PendingTask task = new PendingTask(); + task.setTaskId(taskId); + task.setType("auto_next"); + task.setGroupId(groupId); + task.setExecuteTime(LocalDateTime.now().plusSeconds(delaySeconds)); + pendingTasks.put(taskId, task); + log.info("安排{}秒后自动开始下一期: groupId={}", delaySeconds, groupId); + return taskId; + } + + /** + * 安排机器人发包 + */ + public String scheduleBotSendRp(Long roundId, Long groupId, int delaySeconds) { + String taskId = "bot_send_rp_" + taskIdGenerator.getAndIncrement(); + PendingTask task = new PendingTask(); + task.setTaskId(taskId); + task.setType("bot_send_rp"); + task.setRoundId(roundId); + task.setGroupId(groupId); + task.setExecuteTime(LocalDateTime.now().plusSeconds(delaySeconds)); + pendingTasks.put(taskId, task); + log.info("安排{}秒后机器人发包: roundId={}", delaySeconds, roundId); + return taskId; } /** @@ -199,7 +250,7 @@ public class GameJob { broadcastStartRp(groupId, round.getRoundNo()); // 安排10秒后如果没人发包则机器人自动发 - // TODO: 实现延迟检查逻辑 + scheduleBotSendRp(roundId, groupId, 10); } /** @@ -226,10 +277,11 @@ public class GameJob { RedPacket rp = redPacketService.sendRp(bot.getId(), groupId, new BigDecimal("1"), 3, roundId); - // 设置目标尾数 - // TODO: 实现红包尾数控制 - - log.info("机器人自动发包: roundId={}, rpId={}", roundId, rp.getId()); + // 红包尾数控制: 需要在发红包时预计算各份额金额 + // 确保手气王金额的尾数等于目标尾数 + // 注: 当前实现中红包金额随机分配,实际生产需要调用RedPacketService控制尾数 + log.info("机器人自动发包: roundId={}, rpId={}, targetDigit={}", + roundId, rp.getId(), round != null ? round.getTargetDigit() : "null"); } catch (Exception e) { log.error("机器人发包失败: roundId={}, error={}", roundId, e.getMessage()); } @@ -252,7 +304,7 @@ public class GameJob { GameRound roundAfterSettle = gameRoundMapper.selectById(roundId); if (roundAfterSettle != null && roundAfterSettle.getAutoNext() == 1) { // 5秒后自动开始下一期 - scheduleAutoNext(groupId, 5); + scheduleAutoNextTask(groupId, 5); } // 清理任务 @@ -269,28 +321,25 @@ public class GameJob { */ private void notifyWhiteUsers(Long roundId) { List whites = digitWhiteMapper.selectList(new LambdaQueryWrapper()); + GameRound round = gameRoundMapper.selectById(roundId); + if (round == null) return; + for (DigitWhite white : whites) { User user = userMapper.selectById(white.getUserId()); if (user != null) { - // TODO: 发送推送通知 - log.info("通知白名单用户尾数试算完成: userId={}", user.getId()); + // 通过OpenIM发送通知 + openIMClient.sendUserMessage(String.valueOf(user.getId()), + "尾数试算完成,请前往修改尾数。期号: " + round.getRoundNo()); + log.info("通知白名单用户尾数试算完成: userId={}, roundId={}", user.getId(), roundId); } } } - /** - * 安排自动开始下一期 - */ - private void scheduleAutoNext(Long groupId, int delaySeconds) { - // TODO: 实现延迟自动开局 - log.info("安排{}秒后自动开始下一期", delaySeconds); - } - /** * 广播开始下注 */ private void broadcastStartBet(Long groupId, String roundNo) { - // TODO: 获取群对应的OpenIM群ID并发送 + // 注意: 实际生产中需要通过Group表映射获取OpenIM群ID openIMClient.sendGroupMessage(String.valueOf(groupId), roundNo + "期开始下注"); } @@ -331,4 +380,82 @@ public class GameJob { LocalDateTime endBetTime; LocalDateTime startRpTime; } + + /** + * 延迟任务信息 + */ + private static class PendingTask { + private String taskId; + private String type; + private Long roundId; + private Long groupId; + private Integer digit; + private LocalDateTime executeTime; + + public String getTaskId() { return taskId; } + public void setTaskId(String taskId) { this.taskId = taskId; } + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public Long getRoundId() { return roundId; } + public void setRoundId(Long roundId) { this.roundId = roundId; } + public Long getGroupId() { return groupId; } + public void setGroupId(Long groupId) { this.groupId = groupId; } + public Integer getDigit() { return digit; } + public void setDigit(Integer digit) { this.digit = digit; } + public LocalDateTime getExecuteTime() { return executeTime; } + public void setExecuteTime(LocalDateTime executeTime) { this.executeTime = executeTime; } + } + + /** + * 处理延迟任务队列(每秒执行) + */ + @Scheduled(fixedDelay = 1000) + public void processPendingTasks() { + LocalDateTime now = LocalDateTime.now(); + pendingTasks.entrySet().removeIf(entry -> { + PendingTask task = entry.getValue(); + if (task.getExecuteTime().isBefore(now) || task.getExecuteTime().isEqual(now)) { + executeTask(task); + return true; + } + return false; + }); + } + + /** + * 执行延迟任务 + */ + private void executeTask(PendingTask task) { + try { + switch (task.getType()) { + case "settle": + onSettle(task.getRoundId(), task.getGroupId(), task.getDigit()); + break; + case "auto_next": + onAutoNext(task.getGroupId()); + break; + case "start_rp": + onStartRp(task.getRoundId(), task.getGroupId()); + break; + case "bot_send_rp": + botSendRp(task.getRoundId(), task.getGroupId()); + break; + default: + log.warn("未知任务类型: {}", task.getType()); + } + } catch (Exception e) { + log.error("执行任务失败: taskId={}, error={}", task.getTaskId(), e.getMessage()); + } + } + + /** + * 自动开始下一期 + */ + private void onAutoNext(Long groupId) { + try { + gameService.startGame(groupId, 0L); // 0L表示系统自动 + } catch (Exception e) { + log.error("自动开始下一期失败: groupId={}, error={}", groupId, e.getMessage()); + } + } } diff --git a/src/main/java/com/tailbet/model/vo/GroupStatVo.java b/src/main/java/com/tailbet/model/vo/GroupStatVo.java new file mode 100644 index 0000000..9e2f5e6 --- /dev/null +++ b/src/main/java/com/tailbet/model/vo/GroupStatVo.java @@ -0,0 +1,60 @@ +package com.tailbet.model.vo; + +import lombok.Data; +import java.util.List; + +/** + * 群统计信息 + */ +@Data +public class GroupStatVo { + /** + * 群ID + */ + private Long groupId; + + /** + * 总局数 + */ + private Integer totalRounds; + + /** + * 进行中局数 + */ + private Integer ongoingRounds; + + /** + * 总下注人次 + */ + private Integer totalBetCount; + + /** + * 总下注金额 + */ + private Long totalBetAmount; + + /** + * 总派奖金额 + */ + private Long totalAwardAmount; + + /** + * 庄家总利润 + */ + private Long totalProfit; + + /** + * 各玩法下注统计 + */ + private List playTypeStats; + + /** + * 玩法统计 + */ + @Data + public static class PlayTypeStat { + private String playType; + private Integer betCount; + private Integer betAmount; + } +} diff --git a/src/main/java/com/tailbet/openim/OpenIMCallbackController.java b/src/main/java/com/tailbet/openim/OpenIMCallbackController.java index 090ceaa..6c308b9 100644 --- a/src/main/java/com/tailbet/openim/OpenIMCallbackController.java +++ b/src/main/java/com/tailbet/openim/OpenIMCallbackController.java @@ -139,10 +139,23 @@ public class OpenIMCallbackController { */ private void handleStatCommand(String groupId) { try { - // TODO: 返回统计信息 - sendGroupMessage(groupId, "统计功能开发中..."); + Long bizGroupId = getBizGroupId(groupId); + if (bizGroupId == null) { + sendGroupMessage(groupId, "群未配置"); + return; + } + + var stat = gameService.getGroupStat(bizGroupId); + String message = String.format( + "【本群统计】\n总局数: %d\n进行中: %d\n总下注人次: %d\n总下注金额: %d\n总派奖金额: %d\n庄家利润: %d", + stat.getTotalRounds(), stat.getOngoingRounds(), + stat.getTotalBetCount(), stat.getTotalBetAmount(), + stat.getTotalAwardAmount(), stat.getTotalProfit() + ); + sendGroupMessage(groupId, message); } catch (Exception e) { log.error("处理统计指令失败: {}", e.getMessage()); + sendGroupMessage(groupId, "统计查询失败"); } } @@ -183,7 +196,8 @@ public class OpenIMCallbackController { * 将OpenIM群ID转换为业务群ID */ private Long getBizGroupId(String openimGroupId) { - // TODO: 通过映射表查询 - return 1L; + // 通过GroupService查询映射 + var group = groupService.getByOpenimGroupId(openimGroupId); + return group != null ? group.getId() : null; } } diff --git a/src/main/java/com/tailbet/service/IGameService.java b/src/main/java/com/tailbet/service/IGameService.java index c1aa36c..ef3fed4 100644 --- a/src/main/java/com/tailbet/service/IGameService.java +++ b/src/main/java/com/tailbet/service/IGameService.java @@ -3,6 +3,7 @@ package com.tailbet.service; import com.tailbet.model.entity.GameRound; import com.tailbet.model.entity.DrawRecord; import com.tailbet.model.entity.BetOrder; +import com.tailbet.model.vo.GroupStatVo; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tailbet.model.vo.DigitTrialVo; import lombok.Data; @@ -80,4 +81,9 @@ public interface IGameService { * 获取开奖历史 */ Page getHistory(Long groupId, Integer pageNum, Integer pageSize); + + /** + * 获取群统计信息 + */ + GroupStatVo getGroupStat(Long groupId); } diff --git a/src/main/java/com/tailbet/service/IGroupService.java b/src/main/java/com/tailbet/service/IGroupService.java index 6c48615..23d2716 100644 --- a/src/main/java/com/tailbet/service/IGroupService.java +++ b/src/main/java/com/tailbet/service/IGroupService.java @@ -59,4 +59,9 @@ public interface IGroupService { * 检查用户是否是管理员 */ boolean isAdmin(Long groupId, Long userId); + + /** + * 根据OpenIM群ID获取群信息 + */ + Group getByOpenimGroupId(String openimGroupId); } diff --git a/src/main/java/com/tailbet/service/impl/GameServiceImpl.java b/src/main/java/com/tailbet/service/impl/GameServiceImpl.java index 46499b7..8de5aa5 100644 --- a/src/main/java/com/tailbet/service/impl/GameServiceImpl.java +++ b/src/main/java/com/tailbet/service/impl/GameServiceImpl.java @@ -11,6 +11,7 @@ import com.tailbet.model.entity.DrawRecord; import com.tailbet.model.entity.GameRound; import com.tailbet.model.entity.Group; import com.tailbet.model.vo.DigitTrialVo; +import com.tailbet.model.vo.GroupStatVo; import com.tailbet.service.IGameService; import com.tailbet.service.IPointsService; import com.tailbet.service.IAuditService; @@ -334,4 +335,34 @@ public class GameServiceImpl implements IGameService { .orderByDesc(DrawRecord::getCreateTime); return drawRecordMapper.selectPage(page, wrapper); } + + /** + * 获取群统计信息 + */ + @Override + public GroupStatVo getGroupStat(Long groupId) { + GroupStatVo stat = new GroupStatVo(); + stat.setGroupId(groupId); + + // 统计局数 + List allRounds = gameRoundMapper.selectList(new LambdaQueryWrapper() + .eq(GameRound::getGroupId, groupId)); + stat.setTotalRounds(allRounds.size()); + stat.setOngoingRounds((int) allRounds.stream().filter(r -> r.getStatus() == STATUS_ONGOING).count()); + + // 统计注单 + List allBets = betOrderMapper.selectList(new LambdaQueryWrapper() + .eq(BetOrder::getGroupId, groupId) + .eq(BetOrder::getIsFake, 0)); + stat.setTotalBetCount(allBets.size()); + stat.setTotalBetAmount(allBets.stream().mapToLong(BetOrder::getBetAmount).sum()); + + // 统计派奖 + List records = drawRecordMapper.selectList(new LambdaQueryWrapper() + .eq(DrawRecord::getGroupId, groupId)); + stat.setTotalAwardAmount(records.stream().mapToLong(DrawRecord::getTotalAward).sum()); + stat.setTotalProfit(records.stream().mapToLong(DrawRecord::getProfit).sum()); + + return stat; + } } diff --git a/src/main/java/com/tailbet/service/impl/GroupServiceImpl.java b/src/main/java/com/tailbet/service/impl/GroupServiceImpl.java index 424407f..71170a0 100644 --- a/src/main/java/com/tailbet/service/impl/GroupServiceImpl.java +++ b/src/main/java/com/tailbet/service/impl/GroupServiceImpl.java @@ -211,4 +211,13 @@ public class GroupServiceImpl implements IGroupService { .eq(GroupMember::getUserId, userId)); return member != null && (member.getRole() == ROLE_ADMIN || member.getRole() == ROLE_OWNER); } + + /** + * 根据OpenIM群ID获取群信息 + */ + @Override + public Group getByOpenimGroupId(String openimGroupId) { + return groupMapper.selectOne(new LambdaQueryWrapper() + .eq(Group::getOpenimGroupId, openimGroupId)); + } } diff --git a/src/main/java/com/tailbet/service/impl/PushServiceImpl.java b/src/main/java/com/tailbet/service/impl/PushServiceImpl.java index fc3bdf5..d5a4486 100644 --- a/src/main/java/com/tailbet/service/impl/PushServiceImpl.java +++ b/src/main/java/com/tailbet/service/impl/PushServiceImpl.java @@ -93,11 +93,7 @@ public class PushServiceImpl implements IPushService { return; } - // TODO: 根据实际推送通道实现 - // 1. 如果用户在线,通过OpenIM发送即时消息 - // 2. 如果用户离线,通过FCM/APNs发送离线推送 - - // 通过OpenIM单聊发送 + // 通过OpenIM单聊发送(生产环境可扩展FCM/APNs离线推送) openIMClient.sendUserMessage(String.valueOf(user.getId()), message.getTitle() + " - " + message.getContent());