diff --git a/src/main/java/com/tailbet/common/enums/ErrorCodeEnum.java b/src/main/java/com/tailbet/common/enums/ErrorCodeEnum.java index 0ee3801..30e811b 100644 --- a/src/main/java/com/tailbet/common/enums/ErrorCodeEnum.java +++ b/src/main/java/com/tailbet/common/enums/ErrorCodeEnum.java @@ -683,6 +683,8 @@ public enum ErrorCodeEnum { IM_OPENIM_INVITE_GROUP_FAILED(14607, "error.14607"), /** OpenIM 踢出群成员失败 */ IM_OPENIM_KICK_GROUP_FAILED(14608, "error.14608"), + /** OpenIM 获取群成员列表失败 */ + IM_OPENIM_GET_GROUP_MEMBER_LIST_FAILED(14618, "error.14618"), /** OpenIM 添加好友失败 */ IM_OPENIM_ADD_FRIEND_FAILED(14609, "error.14609"), /** OpenIM 删除好友失败 */ diff --git a/src/main/java/com/tailbet/controller/GameController.java b/src/main/java/com/tailbet/controller/GameController.java index 0a31b22..a858ee5 100644 --- a/src/main/java/com/tailbet/controller/GameController.java +++ b/src/main/java/com/tailbet/controller/GameController.java @@ -39,16 +39,13 @@ public class GameController { */ @PostMapping("/start") public R startGame(@RequestBody GameOperateDTO dto) { - try { - Long userId = UserContext.getUserId(); - var round = gameService.startGame(dto.getGroupId(), userId); - // 触发游戏开始后的定时器和广播 - int betWindowSeconds = gameJob.getConfigInt("bet_window_seconds", 60); - gameJob.onGameStart(round.getId(), dto.getGroupId(), betWindowSeconds); - return R.ok(round); - } catch (RuntimeException e) { - return R.fail(e.getMessage()); - } + Long userId = UserContext.getUserId(); + var round = gameService.startGame(dto.getGroupId(), userId); + // 触发游戏开始后的定时器和广播 + int betWindowSeconds = gameJob.getConfigInt("bet_window_seconds", 60); + gameJob.onGameStart(round.getId(), dto.getGroupId(), betWindowSeconds); + return R.ok(round); + } /** @@ -56,15 +53,12 @@ public class GameController { */ @PostMapping("/end") public R endBet(@RequestBody GameOperateDTO dto) { - try { - Long userId = UserContext.getUserId(); - var round = gameService.endBet(dto.getGroupId(), userId); - // 触发结束接注后的广播和发红包流程 - gameJob.onEndBet(round.getId(), dto.getGroupId()); - return R.ok(round); - } catch (RuntimeException e) { - return R.fail(e.getMessage()); - } + Long userId = UserContext.getUserId(); + var round = gameService.endBet(dto.getGroupId(), userId); + // 触发结束接注后的广播和发红包流程 + gameJob.onEndBet(round.getId(), dto.getGroupId()); + return R.ok(round); + } /** @@ -72,13 +66,10 @@ public class GameController { */ @PostMapping("/stop") public R stopAutoNext(@RequestBody GameOperateDTO dto) { - try { - Long userId = UserContext.getUserId(); - gameService.stopAutoNext(dto.getGroupId(), userId); - return R.ok(); - } catch (RuntimeException e) { - return R.fail(e.getMessage()); - } + Long userId = UserContext.getUserId(); + gameService.stopAutoNext(dto.getGroupId(), userId); + return R.ok(); + } /** @@ -86,17 +77,14 @@ public class GameController { */ @PostMapping("/bet") public R bet(@RequestBody BetDTO dto) { - try { - Long userId = UserContext.getUserId(); - var bet = betService.placeBet(userId, dto.getGroupId(), - dto.getClientMsgId(), dto.getPlayType(), dto.getBetAmount()); - if (bet == null) { - return R.ok(); - } - return R.ok(bet); - } catch (RuntimeException e) { - return R.fail(e.getMessage()); + Long userId = UserContext.getUserId(); + var bet = betService.placeBet(userId, dto.getGroupId(), + dto.getClientMsgId(), dto.getPlayType(), dto.getBetAmount()); + if (bet == null) { + return R.ok(); } + return R.ok(bet); + } /** @@ -104,12 +92,9 @@ public class GameController { */ @GetMapping("/bet/trial") public R> trialDigit(Long groupId, Long roundId) { - try { - List trials = gameService.trialDigit(groupId, roundId); - return R.ok(trials); - } catch (RuntimeException e) { - return R.fail(e.getMessage()); - } + List trials = gameService.trialDigit(groupId, roundId); + return R.ok(trials); + } /** @@ -126,20 +111,17 @@ public class GameController { */ @PostMapping("/digit/set") public R setDigit(@RequestBody SetDigitDTO dto) { - try { - Long userId = UserContext.getUserId(); + Long userId = UserContext.getUserId(); - // 验证白名单权限 - DigitWhite white = digitWhiteMapper.selectOne(new LambdaQueryWrapper() - .eq(DigitWhite::getUserId, userId)); - if (white == null) { - return R.fail("您不是白名单用户,无权设置尾数"); - } - - gameService.setTargetDigit(dto.getGroupId(), dto.getDigit(), userId); - return R.ok(); - } catch (RuntimeException e) { - return R.fail(e.getMessage()); + // 验证白名单权限 + DigitWhite white = digitWhiteMapper.selectOne(new LambdaQueryWrapper() + .eq(DigitWhite::getUserId, userId)); + if (white == null) { + return R.fail("您不是白名单用户,无权设置尾数"); } + + gameService.setTargetDigit(dto.getGroupId(), dto.getDigit(), userId); + return R.ok(); + } } diff --git a/src/main/java/com/tailbet/controller/RedPacketController.java b/src/main/java/com/tailbet/controller/RedPacketController.java index a590876..d7e64bf 100644 --- a/src/main/java/com/tailbet/controller/RedPacketController.java +++ b/src/main/java/com/tailbet/controller/RedPacketController.java @@ -6,6 +6,7 @@ import com.tailbet.model.entity.RedPacket; import com.tailbet.model.entity.RedPacketRecv; import com.tailbet.model.vo.R; import com.tailbet.model.vo.UserContext; +import com.tailbet.service.IGameService; import com.tailbet.service.IRedPacketService; import com.tailbet.util.RedisLockUtil; import lombok.RequiredArgsConstructor; @@ -20,20 +21,21 @@ import org.springframework.web.bind.annotation.*; public class RedPacketController { private final IRedPacketService redPacketService; + private final IGameService gameService; /** * 发红包 */ @PostMapping("/send") public R sendRp(@RequestBody SendRpDTO dto) { - try { - Long userId = UserContext.getUserId(); - var rp = redPacketService.sendRp(userId, dto.getGroupId(), - dto.getTotalAmount(), dto.getTotalCount(), dto.getRoundId()); - return R.ok(rp); - } catch (RuntimeException e) { - return R.fail(e.getMessage()); - } + Long userId = UserContext.getUserId(); + // roundId从当前group的进行中局获取 + var ongoingRound = gameService.getOngoingRound(dto.getGroupId()); + Long roundId = ongoingRound == null ? null : ongoingRound.getId(); + var rp = redPacketService.sendRp(userId, dto.getGroupId(), + dto.getTotalAmount(), dto.getTotalCount(), roundId); + return R.ok(rp); + } /** @@ -41,18 +43,15 @@ public class RedPacketController { */ @PostMapping("/receive") public R receiveRp(@RequestBody ReceiveRpDTO dto) { - try { - Long userId = UserContext.getUserId(); - // 使用分布式锁防止多领 - var recv = RedisLockUtil.lock("rp:receive:" + dto.getRpId(), - () -> redPacketService.receiveRp(dto.getRpId(), userId)); - if (recv == null) { - return R.fail("领取失败,请重试"); - } - return R.ok(recv); - } catch (RuntimeException e) { - return R.fail(e.getMessage()); + Long userId = UserContext.getUserId(); + // 使用分布式锁防止多领 + var recv = RedisLockUtil.lock("rp:receive:" + dto.getRpId(), + () -> redPacketService.receiveRp(dto.getRpId(), userId)); + if (recv == null) { + return R.fail("领取失败,请重试"); } + return R.ok(recv); + } /** diff --git a/src/main/java/com/tailbet/job/GameJob.java b/src/main/java/com/tailbet/job/GameJob.java index 3ad4525..60d4b91 100644 --- a/src/main/java/com/tailbet/job/GameJob.java +++ b/src/main/java/com/tailbet/job/GameJob.java @@ -100,7 +100,7 @@ public class GameJob { try { List ongoingRounds = gameRoundMapper.selectList( new LambdaQueryWrapper() - .eq(GameRound::getStatus, 0)); + .lt(GameRound::getStatus, IGameService.STATUS_FINISHED)); if (ongoingRounds.isEmpty()) { log.info("无进行中的局,无需恢复"); @@ -269,7 +269,7 @@ public class GameJob { @Scheduled(fixedDelay = 10000) public void checkRoundTimeout() { List ongoingRounds = gameRoundMapper.selectList(new LambdaQueryWrapper() - .eq(GameRound::getStatus, 0)); + .eq(GameRound::getStatus, IGameService.STATUS_BETTING)); for (GameRound round : ongoingRounds) { // 检查是否超过接注时间 @@ -452,6 +452,11 @@ public class GameJob { return; } + // 更新局状态为开始发红包 + round.setStatus(IGameService.STATUS_RP_SENDING); + round.setStartRpTime(LocalDateTime.now()); + gameRoundMapper.updateById(round); + // 广播开始发红包 broadcastStartRp(groupId, round.getRoundNo()); diff --git a/src/main/java/com/tailbet/model/dto/SendRpDTO.java b/src/main/java/com/tailbet/model/dto/SendRpDTO.java index 4a795f2..12e826e 100644 --- a/src/main/java/com/tailbet/model/dto/SendRpDTO.java +++ b/src/main/java/com/tailbet/model/dto/SendRpDTO.java @@ -22,9 +22,4 @@ public class SendRpDTO { * 红包个数 */ private Integer totalCount; - - /** - * 绑定的局ID(可选) - */ - private Long roundId; } diff --git a/src/main/java/com/tailbet/model/entity/GameRound.java b/src/main/java/com/tailbet/model/entity/GameRound.java index 3468900..1b76e4a 100644 --- a/src/main/java/com/tailbet/model/entity/GameRound.java +++ b/src/main/java/com/tailbet/model/entity/GameRound.java @@ -27,7 +27,7 @@ public class GameRound { private String roundNo; /** - * 状态:0进行中 1已结束 + * 状态:0开始下注 1结束下注 2开始发红包 3已结束 */ private Integer status; diff --git a/src/main/java/com/tailbet/openim/OpenIMApiClient.java b/src/main/java/com/tailbet/openim/OpenIMApiClient.java index eb7ca21..cbd2b3b 100644 --- a/src/main/java/com/tailbet/openim/OpenIMApiClient.java +++ b/src/main/java/com/tailbet/openim/OpenIMApiClient.java @@ -1583,6 +1583,64 @@ public class OpenIMApiClient { } } + /** + * 获取群成员列表 + * 对应接口:POST /group/get_group_member_list + *

+ * pagination.showNumber 每页数量必须大于 0。 + * filter 保留字段,入口校验取值范围 0-5,当前实现不使用该字段过滤结果。 + *

+ * + * @param groupID 群组 ID + * @param pageNumber 页码,从 1 开始 + * @param showNumber 每页数量,必须大于 0 + * @param keyword 按用户 ID 或昵称过滤(可选) + * @return 群成员信息列表 + */ + @SuppressWarnings("unchecked") + public List> getGroupMemberList(String groupID, int pageNumber, int showNumber, String keyword) { + String url = openIMProperties.resolveServerApiUrl() + "/group/get_group_member_list"; + + Map pagination = new HashMap<>(); + pagination.put("pageNumber", pageNumber); + pagination.put("showNumber", showNumber); + + Map body = new HashMap<>(); + body.put("pagination", pagination); + body.put("groupID", groupID); +// body.put("filter", filter); + if (keyword != null && !keyword.isBlank()) { + body.put("keyword", keyword); + } + + OpenIMBaseResp resp = doPost(url, body); + if (!isSuccess(resp)) { + String errMsg = resp.getErrMsg() != null ? resp.getErrMsg() : ""; + // 群组不存在(重复拉取时触发),返回空列表 + if (resp.getErrCode() == 1004 + || errMsg.contains("RecordNotFound") + || errMsg.contains("not found") + || errMsg.contains("GroupIDNotFound")) { + log.debug("【OpenIM】获取群成员列表:群组不存在: groupID={}", groupID); + return Collections.emptyList(); + } + log.error("【OpenIM】获取群成员列表失败: groupID={}, pageNumber={}, showNumber={}, errCode={}, errMsg={}", + groupID, pageNumber, showNumber, resp.getErrCode(), resp.getErrMsg()); + throw new BusinessException(ErrorCodeEnum.IM_OPENIM_GET_GROUP_MEMBER_LIST_FAILED, resp.getErrMsg()); + } + + Map data = (Map) resp.getData(); + if (data == null) { + return Collections.emptyList(); + } + + Object membersObj = data.get("members"); + if (!(membersObj instanceof List)) { + return Collections.emptyList(); + } + return (List>) membersObj; + } + /** * 批量查询 OpenIM 中存在的群组 ID 集合(验证专用) * 对应接口:POST /group/get_groups_info diff --git a/src/main/java/com/tailbet/service/IGameService.java b/src/main/java/com/tailbet/service/IGameService.java index ef3fed4..9f56af2 100644 --- a/src/main/java/com/tailbet/service/IGameService.java +++ b/src/main/java/com/tailbet/service/IGameService.java @@ -17,8 +17,14 @@ import java.util.List; public interface IGameService { // 局状态常量 - int STATUS_ONGOING = 0; - int STATUS_ENDED = 1; + int STATUS_BETTING = 0; // 开始下注 + int STATUS_BET_CLOSED = 1; // 结束下注,等待发红包 + int STATUS_RP_SENDING = 2; // 开始发红包,等待红包领完 + int STATUS_FINISHED = 3; // 已结束 + + // 向后兼容别名 + int STATUS_ONGOING = STATUS_BETTING; + int STATUS_ENDED = STATUS_FINISHED; // 玩法赔率 BigDecimal ODDS_SINGLE_DOUBLE = new BigDecimal("1"); @@ -32,6 +38,11 @@ public interface IGameService { */ GameRound getOngoingRound(Long groupId); + /** + * 获取群当前正在下注的局 + */ + GameRound getBettingRound(Long groupId); + /** * 生成期号 */ diff --git a/src/main/java/com/tailbet/service/impl/BetServiceImpl.java b/src/main/java/com/tailbet/service/impl/BetServiceImpl.java index 6fbaebf..5859a99 100644 --- a/src/main/java/com/tailbet/service/impl/BetServiceImpl.java +++ b/src/main/java/com/tailbet/service/impl/BetServiceImpl.java @@ -61,12 +61,12 @@ public class BetServiceImpl implements IBetService { return null; } - // 检查进行中的局 + // 检查进行中的局(下注中) GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper() .eq(GameRound::getGroupId, groupId) - .eq(GameRound::getStatus, 0)); + .eq(GameRound::getStatus, IGameService.STATUS_BETTING)); if (round == null) { - throw new RuntimeException("当前没有进行中的局"); + throw new RuntimeException("当前不在下注阶段"); } // 检查余额 diff --git a/src/main/java/com/tailbet/service/impl/FakeUserServiceImpl.java b/src/main/java/com/tailbet/service/impl/FakeUserServiceImpl.java index 33e74ca..47f0bd2 100644 --- a/src/main/java/com/tailbet/service/impl/FakeUserServiceImpl.java +++ b/src/main/java/com/tailbet/service/impl/FakeUserServiceImpl.java @@ -143,10 +143,10 @@ public class FakeUserServiceImpl implements IFakeUserService { String fakeIdStr = fakeList.get(RANDOM.nextInt(fakeList.size())); Long fakeId = Long.parseLong(fakeIdStr); - // 检查是否有进行中的局 + // 检查是否有下注中的局 GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper() .eq(GameRound::getGroupId, groupId) - .eq(GameRound::getStatus, 0)); + .eq(GameRound::getStatus, com.tailbet.service.IGameService.STATUS_BETTING)); if (round == null) { return; diff --git a/src/main/java/com/tailbet/service/impl/GameServiceImpl.java b/src/main/java/com/tailbet/service/impl/GameServiceImpl.java index 8de5aa5..c908179 100644 --- a/src/main/java/com/tailbet/service/impl/GameServiceImpl.java +++ b/src/main/java/com/tailbet/service/impl/GameServiceImpl.java @@ -44,13 +44,27 @@ public class GameServiceImpl implements IGameService { private final IAuditService auditService; /** - * 获取群当前进行中的局 + * 获取群当前未结束的局(包含下注中、已封盘、发红包中) */ @Override public GameRound getOngoingRound(Long groupId) { return gameRoundMapper.selectOne(new LambdaQueryWrapper() .eq(GameRound::getGroupId, groupId) - .eq(GameRound::getStatus, STATUS_ONGOING)); + .lt(GameRound::getStatus, STATUS_FINISHED) + .orderByDesc(GameRound::getCreateTime) + .last("LIMIT 1")); + } + + /** + * 获取群当前正在下注中的局 + */ + @Override + public GameRound getBettingRound(Long groupId) { + return gameRoundMapper.selectOne(new LambdaQueryWrapper() + .eq(GameRound::getGroupId, groupId) + .eq(GameRound::getStatus, STATUS_BETTING) + .orderByDesc(GameRound::getCreateTime) + .last("LIMIT 1")); } /** @@ -97,7 +111,7 @@ public class GameServiceImpl implements IGameService { GameRound round = new GameRound(); round.setGroupId(groupId); round.setRoundNo(generateRoundNo(groupId)); - round.setStatus(STATUS_ONGOING); + round.setStatus(STATUS_BETTING); round.setStartTime(LocalDateTime.now()); round.setDigitStrategy("random"); round.setAutoNext(1); @@ -116,18 +130,20 @@ public class GameServiceImpl implements IGameService { @Override @Transactional public GameRound endBet(Long groupId, Long userId) { - GameRound round = getOngoingRound(groupId); + GameRound round = getBettingRound(groupId); if (round == null) { - throw new RuntimeException("没有进行中的局"); + throw new RuntimeException("当前没有下注中的局"); } - // 校验群主权限 - Group group = groupMapper.selectById(groupId); - if (!group.getOwnerUserId().equals(userId)) { - throw new RuntimeException("只有群主可以结束接注"); + // 校验群主权限(系统调用时userId=0L跳过校验) + if (!Long.valueOf(0L).equals(userId)) { + Group group = groupMapper.selectById(groupId); + if (!group.getOwnerUserId().equals(userId)) { + throw new RuntimeException("只有群主可以结束接注"); + } } - round.setStatus(STATUS_ENDED); + round.setStatus(STATUS_BET_CLOSED); round.setEndBetTime(LocalDateTime.now()); gameRoundMapper.updateById(round); @@ -165,9 +181,9 @@ public class GameServiceImpl implements IGameService { @Override @Transactional public void setTargetDigit(Long groupId, Integer digit, Long userId) { - GameRound round = getOngoingRound(groupId); + GameRound round = getBettingRound(groupId); if (round == null) { - throw new RuntimeException("没有进行中的局"); + throw new RuntimeException("当前没有下注中的局"); } round.setTargetDigit(digit); @@ -315,7 +331,7 @@ public class GameServiceImpl implements IGameService { // 更新局状态 round.setFinishTime(LocalDateTime.now()); - round.setStatus(STATUS_ENDED); + round.setStatus(STATUS_FINISHED); gameRoundMapper.updateById(round); // 审计日志 @@ -348,7 +364,7 @@ public class GameServiceImpl implements IGameService { 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()); + stat.setOngoingRounds((int) allRounds.stream().filter(r -> r.getStatus() != null && r.getStatus() < STATUS_FINISHED).count()); // 统计注单 List allBets = betOrderMapper.selectList(new LambdaQueryWrapper()