GameRound status 调整

This commit is contained in:
wells
2026-08-08 22:00:39 +08:00
parent 52061b9d59
commit cfb5b4f2a8
11 changed files with 171 additions and 103 deletions
@@ -683,6 +683,8 @@ public enum ErrorCodeEnum {
IM_OPENIM_INVITE_GROUP_FAILED(14607, "error.14607"), IM_OPENIM_INVITE_GROUP_FAILED(14607, "error.14607"),
/** OpenIM 踢出群成员失败 */ /** OpenIM 踢出群成员失败 */
IM_OPENIM_KICK_GROUP_FAILED(14608, "error.14608"), IM_OPENIM_KICK_GROUP_FAILED(14608, "error.14608"),
/** OpenIM 获取群成员列表失败 */
IM_OPENIM_GET_GROUP_MEMBER_LIST_FAILED(14618, "error.14618"),
/** OpenIM 添加好友失败 */ /** OpenIM 添加好友失败 */
IM_OPENIM_ADD_FRIEND_FAILED(14609, "error.14609"), IM_OPENIM_ADD_FRIEND_FAILED(14609, "error.14609"),
/** OpenIM 删除好友失败 */ /** OpenIM 删除好友失败 */
@@ -39,16 +39,13 @@ public class GameController {
*/ */
@PostMapping("/start") @PostMapping("/start")
public R<GameRound> startGame(@RequestBody GameOperateDTO dto) { public R<GameRound> startGame(@RequestBody GameOperateDTO dto) {
try {
Long userId = UserContext.getUserId(); Long userId = UserContext.getUserId();
var round = gameService.startGame(dto.getGroupId(), userId); var round = gameService.startGame(dto.getGroupId(), userId);
// 触发游戏开始后的定时器和广播 // 触发游戏开始后的定时器和广播
int betWindowSeconds = gameJob.getConfigInt("bet_window_seconds", 60); int betWindowSeconds = gameJob.getConfigInt("bet_window_seconds", 60);
gameJob.onGameStart(round.getId(), dto.getGroupId(), betWindowSeconds); gameJob.onGameStart(round.getId(), dto.getGroupId(), betWindowSeconds);
return R.ok(round); return R.ok(round);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
} }
/** /**
@@ -56,15 +53,12 @@ public class GameController {
*/ */
@PostMapping("/end") @PostMapping("/end")
public R<GameRound> endBet(@RequestBody GameOperateDTO dto) { public R<GameRound> endBet(@RequestBody GameOperateDTO dto) {
try {
Long userId = UserContext.getUserId(); Long userId = UserContext.getUserId();
var round = gameService.endBet(dto.getGroupId(), userId); var round = gameService.endBet(dto.getGroupId(), userId);
// 触发结束接注后的广播和发红包流程 // 触发结束接注后的广播和发红包流程
gameJob.onEndBet(round.getId(), dto.getGroupId()); gameJob.onEndBet(round.getId(), dto.getGroupId());
return R.ok(round); return R.ok(round);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
} }
/** /**
@@ -72,13 +66,10 @@ public class GameController {
*/ */
@PostMapping("/stop") @PostMapping("/stop")
public R<Void> stopAutoNext(@RequestBody GameOperateDTO dto) { public R<Void> stopAutoNext(@RequestBody GameOperateDTO dto) {
try {
Long userId = UserContext.getUserId(); Long userId = UserContext.getUserId();
gameService.stopAutoNext(dto.getGroupId(), userId); gameService.stopAutoNext(dto.getGroupId(), userId);
return R.ok(); return R.ok();
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
} }
/** /**
@@ -86,7 +77,6 @@ public class GameController {
*/ */
@PostMapping("/bet") @PostMapping("/bet")
public R<BetOrder> bet(@RequestBody BetDTO dto) { public R<BetOrder> bet(@RequestBody BetDTO dto) {
try {
Long userId = UserContext.getUserId(); Long userId = UserContext.getUserId();
var bet = betService.placeBet(userId, dto.getGroupId(), var bet = betService.placeBet(userId, dto.getGroupId(),
dto.getClientMsgId(), dto.getPlayType(), dto.getBetAmount()); dto.getClientMsgId(), dto.getPlayType(), dto.getBetAmount());
@@ -94,9 +84,7 @@ public class GameController {
return R.ok(); return R.ok();
} }
return R.ok(bet); return R.ok(bet);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
} }
/** /**
@@ -104,12 +92,9 @@ public class GameController {
*/ */
@GetMapping("/bet/trial") @GetMapping("/bet/trial")
public R<List<DigitTrialVo>> trialDigit(Long groupId, Long roundId) { public R<List<DigitTrialVo>> trialDigit(Long groupId, Long roundId) {
try {
List<DigitTrialVo> trials = gameService.trialDigit(groupId, roundId); List<DigitTrialVo> trials = gameService.trialDigit(groupId, roundId);
return R.ok(trials); return R.ok(trials);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
} }
/** /**
@@ -126,7 +111,6 @@ public class GameController {
*/ */
@PostMapping("/digit/set") @PostMapping("/digit/set")
public R<Void> setDigit(@RequestBody SetDigitDTO dto) { public R<Void> setDigit(@RequestBody SetDigitDTO dto) {
try {
Long userId = UserContext.getUserId(); Long userId = UserContext.getUserId();
// 验证白名单权限 // 验证白名单权限
@@ -138,8 +122,6 @@ public class GameController {
gameService.setTargetDigit(dto.getGroupId(), dto.getDigit(), userId); gameService.setTargetDigit(dto.getGroupId(), dto.getDigit(), userId);
return R.ok(); return R.ok();
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
} }
} }
@@ -6,6 +6,7 @@ import com.tailbet.model.entity.RedPacket;
import com.tailbet.model.entity.RedPacketRecv; import com.tailbet.model.entity.RedPacketRecv;
import com.tailbet.model.vo.R; import com.tailbet.model.vo.R;
import com.tailbet.model.vo.UserContext; import com.tailbet.model.vo.UserContext;
import com.tailbet.service.IGameService;
import com.tailbet.service.IRedPacketService; import com.tailbet.service.IRedPacketService;
import com.tailbet.util.RedisLockUtil; import com.tailbet.util.RedisLockUtil;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -20,20 +21,21 @@ import org.springframework.web.bind.annotation.*;
public class RedPacketController { public class RedPacketController {
private final IRedPacketService redPacketService; private final IRedPacketService redPacketService;
private final IGameService gameService;
/** /**
* 发红包 * 发红包
*/ */
@PostMapping("/send") @PostMapping("/send")
public R<RedPacket> sendRp(@RequestBody SendRpDTO dto) { public R<RedPacket> sendRp(@RequestBody SendRpDTO dto) {
try {
Long userId = UserContext.getUserId(); 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(), var rp = redPacketService.sendRp(userId, dto.getGroupId(),
dto.getTotalAmount(), dto.getTotalCount(), dto.getRoundId()); dto.getTotalAmount(), dto.getTotalCount(), roundId);
return R.ok(rp); return R.ok(rp);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
} }
/** /**
@@ -41,7 +43,6 @@ public class RedPacketController {
*/ */
@PostMapping("/receive") @PostMapping("/receive")
public R<RedPacketRecv> receiveRp(@RequestBody ReceiveRpDTO dto) { public R<RedPacketRecv> receiveRp(@RequestBody ReceiveRpDTO dto) {
try {
Long userId = UserContext.getUserId(); Long userId = UserContext.getUserId();
// 使用分布式锁防止多领 // 使用分布式锁防止多领
var recv = RedisLockUtil.lock("rp:receive:" + dto.getRpId(), var recv = RedisLockUtil.lock("rp:receive:" + dto.getRpId(),
@@ -50,9 +51,7 @@ public class RedPacketController {
return R.fail("领取失败,请重试"); return R.fail("领取失败,请重试");
} }
return R.ok(recv); return R.ok(recv);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
} }
/** /**
+7 -2
View File
@@ -100,7 +100,7 @@ public class GameJob {
try { try {
List<GameRound> ongoingRounds = gameRoundMapper.selectList( List<GameRound> ongoingRounds = gameRoundMapper.selectList(
new LambdaQueryWrapper<GameRound>() new LambdaQueryWrapper<GameRound>()
.eq(GameRound::getStatus, 0)); .lt(GameRound::getStatus, IGameService.STATUS_FINISHED));
if (ongoingRounds.isEmpty()) { if (ongoingRounds.isEmpty()) {
log.info("无进行中的局,无需恢复"); log.info("无进行中的局,无需恢复");
@@ -269,7 +269,7 @@ public class GameJob {
@Scheduled(fixedDelay = 10000) @Scheduled(fixedDelay = 10000)
public void checkRoundTimeout() { public void checkRoundTimeout() {
List<GameRound> ongoingRounds = gameRoundMapper.selectList(new LambdaQueryWrapper<GameRound>() List<GameRound> ongoingRounds = gameRoundMapper.selectList(new LambdaQueryWrapper<GameRound>()
.eq(GameRound::getStatus, 0)); .eq(GameRound::getStatus, IGameService.STATUS_BETTING));
for (GameRound round : ongoingRounds) { for (GameRound round : ongoingRounds) {
// 检查是否超过接注时间 // 检查是否超过接注时间
@@ -452,6 +452,11 @@ public class GameJob {
return; return;
} }
// 更新局状态为开始发红包
round.setStatus(IGameService.STATUS_RP_SENDING);
round.setStartRpTime(LocalDateTime.now());
gameRoundMapper.updateById(round);
// 广播开始发红包 // 广播开始发红包
broadcastStartRp(groupId, round.getRoundNo()); broadcastStartRp(groupId, round.getRoundNo());
@@ -22,9 +22,4 @@ public class SendRpDTO {
* 红包个数 * 红包个数
*/ */
private Integer totalCount; private Integer totalCount;
/**
* 绑定的局ID(可选)
*/
private Long roundId;
} }
@@ -27,7 +27,7 @@ public class GameRound {
private String roundNo; private String roundNo;
/** /**
* 状态:0进行中 1已结束 * 状态:0开始下注 1结束下注 2开始发红包 3已结束
*/ */
private Integer status; private Integer status;
@@ -1583,6 +1583,64 @@ public class OpenIMApiClient {
} }
} }
/**
* 获取群成员列表
* 对应接口:POST /group/get_group_member_list
* <p>
* pagination.showNumber 每页数量必须大于 0。
* filter 保留字段,入口校验取值范围 0-5,当前实现不使用该字段过滤结果。
* </p>
*
* @param groupID 群组 ID
* @param pageNumber 页码,从 1 开始
* @param showNumber 每页数量,必须大于 0
* @param keyword 按用户 ID 或昵称过滤(可选)
* @return 群成员信息列表
*/
@SuppressWarnings("unchecked")
public List<Map<String, Object>> getGroupMemberList(String groupID, int pageNumber, int showNumber, String keyword) {
String url = openIMProperties.resolveServerApiUrl() + "/group/get_group_member_list";
Map<String, Object> pagination = new HashMap<>();
pagination.put("pageNumber", pageNumber);
pagination.put("showNumber", showNumber);
Map<String, Object> 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<String, Object> data = (Map<String, Object>) resp.getData();
if (data == null) {
return Collections.emptyList();
}
Object membersObj = data.get("members");
if (!(membersObj instanceof List<?>)) {
return Collections.emptyList();
}
return (List<Map<String, Object>>) membersObj;
}
/** /**
* 批量查询 OpenIM 中存在的群组 ID 集合(验证专用) * 批量查询 OpenIM 中存在的群组 ID 集合(验证专用)
* 对应接口:POST /group/get_groups_info * 对应接口:POST /group/get_groups_info
@@ -17,8 +17,14 @@ import java.util.List;
public interface IGameService { public interface IGameService {
// 局状态常量 // 局状态常量
int STATUS_ONGOING = 0; int STATUS_BETTING = 0; // 开始下注
int STATUS_ENDED = 1; 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"); BigDecimal ODDS_SINGLE_DOUBLE = new BigDecimal("1");
@@ -32,6 +38,11 @@ public interface IGameService {
*/ */
GameRound getOngoingRound(Long groupId); GameRound getOngoingRound(Long groupId);
/**
* 获取群当前正在下注的局
*/
GameRound getBettingRound(Long groupId);
/** /**
* 生成期号 * 生成期号
*/ */
@@ -61,12 +61,12 @@ public class BetServiceImpl implements IBetService {
return null; return null;
} }
// 检查进行中的局 // 检查进行中的局(下注中)
GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>() GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>()
.eq(GameRound::getGroupId, groupId) .eq(GameRound::getGroupId, groupId)
.eq(GameRound::getStatus, 0)); .eq(GameRound::getStatus, IGameService.STATUS_BETTING));
if (round == null) { if (round == null) {
throw new RuntimeException("当前没有进行中的局"); throw new RuntimeException("当前不在下注阶段");
} }
// 检查余额 // 检查余额
@@ -143,10 +143,10 @@ public class FakeUserServiceImpl implements IFakeUserService {
String fakeIdStr = fakeList.get(RANDOM.nextInt(fakeList.size())); String fakeIdStr = fakeList.get(RANDOM.nextInt(fakeList.size()));
Long fakeId = Long.parseLong(fakeIdStr); Long fakeId = Long.parseLong(fakeIdStr);
// 检查是否有进行中的局 // 检查是否有下注中的局
GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>() GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>()
.eq(GameRound::getGroupId, groupId) .eq(GameRound::getGroupId, groupId)
.eq(GameRound::getStatus, 0)); .eq(GameRound::getStatus, com.tailbet.service.IGameService.STATUS_BETTING));
if (round == null) { if (round == null) {
return; return;
@@ -44,13 +44,27 @@ public class GameServiceImpl implements IGameService {
private final IAuditService auditService; private final IAuditService auditService;
/** /**
* 获取群当前进行中的局 * 获取群当前未结束的局(包含下注中、已封盘、发红包中)
*/ */
@Override @Override
public GameRound getOngoingRound(Long groupId) { public GameRound getOngoingRound(Long groupId) {
return gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>() return gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>()
.eq(GameRound::getGroupId, groupId) .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<GameRound>()
.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(); GameRound round = new GameRound();
round.setGroupId(groupId); round.setGroupId(groupId);
round.setRoundNo(generateRoundNo(groupId)); round.setRoundNo(generateRoundNo(groupId));
round.setStatus(STATUS_ONGOING); round.setStatus(STATUS_BETTING);
round.setStartTime(LocalDateTime.now()); round.setStartTime(LocalDateTime.now());
round.setDigitStrategy("random"); round.setDigitStrategy("random");
round.setAutoNext(1); round.setAutoNext(1);
@@ -116,18 +130,20 @@ public class GameServiceImpl implements IGameService {
@Override @Override
@Transactional @Transactional
public GameRound endBet(Long groupId, Long userId) { public GameRound endBet(Long groupId, Long userId) {
GameRound round = getOngoingRound(groupId); GameRound round = getBettingRound(groupId);
if (round == null) { if (round == null) {
throw new RuntimeException("没有进行中的局"); throw new RuntimeException("当前没有下注中的局");
} }
// 校验群主权限 // 校验群主权限(系统调用时userId=0L跳过校验)
if (!Long.valueOf(0L).equals(userId)) {
Group group = groupMapper.selectById(groupId); Group group = groupMapper.selectById(groupId);
if (!group.getOwnerUserId().equals(userId)) { if (!group.getOwnerUserId().equals(userId)) {
throw new RuntimeException("只有群主可以结束接注"); throw new RuntimeException("只有群主可以结束接注");
} }
}
round.setStatus(STATUS_ENDED); round.setStatus(STATUS_BET_CLOSED);
round.setEndBetTime(LocalDateTime.now()); round.setEndBetTime(LocalDateTime.now());
gameRoundMapper.updateById(round); gameRoundMapper.updateById(round);
@@ -165,9 +181,9 @@ public class GameServiceImpl implements IGameService {
@Override @Override
@Transactional @Transactional
public void setTargetDigit(Long groupId, Integer digit, Long userId) { public void setTargetDigit(Long groupId, Integer digit, Long userId) {
GameRound round = getOngoingRound(groupId); GameRound round = getBettingRound(groupId);
if (round == null) { if (round == null) {
throw new RuntimeException("没有进行中的局"); throw new RuntimeException("当前没有下注中的局");
} }
round.setTargetDigit(digit); round.setTargetDigit(digit);
@@ -315,7 +331,7 @@ public class GameServiceImpl implements IGameService {
// 更新局状态 // 更新局状态
round.setFinishTime(LocalDateTime.now()); round.setFinishTime(LocalDateTime.now());
round.setStatus(STATUS_ENDED); round.setStatus(STATUS_FINISHED);
gameRoundMapper.updateById(round); gameRoundMapper.updateById(round);
// 审计日志 // 审计日志
@@ -348,7 +364,7 @@ public class GameServiceImpl implements IGameService {
List<GameRound> allRounds = gameRoundMapper.selectList(new LambdaQueryWrapper<GameRound>() List<GameRound> allRounds = gameRoundMapper.selectList(new LambdaQueryWrapper<GameRound>()
.eq(GameRound::getGroupId, groupId)); .eq(GameRound::getGroupId, groupId));
stat.setTotalRounds(allRounds.size()); 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<BetOrder> allBets = betOrderMapper.selectList(new LambdaQueryWrapper<BetOrder>() List<BetOrder> allBets = betOrderMapper.selectList(new LambdaQueryWrapper<BetOrder>()