diff --git a/src/main/java/com/tailbet/controller/GameController.java b/src/main/java/com/tailbet/controller/GameController.java index a858ee5..f1d0a2f 100644 --- a/src/main/java/com/tailbet/controller/GameController.java +++ b/src/main/java/com/tailbet/controller/GameController.java @@ -112,11 +112,7 @@ public class GameController { @PostMapping("/digit/set") public R setDigit(@RequestBody SetDigitDTO dto) { Long userId = UserContext.getUserId(); - - // 验证白名单权限 - DigitWhite white = digitWhiteMapper.selectOne(new LambdaQueryWrapper() - .eq(DigitWhite::getUserId, userId)); - if (white == null) { + if (!UserContext.isWhite()) { return R.fail("您不是白名单用户,无权设置尾数"); } diff --git a/src/main/java/com/tailbet/model/entity/RedPacket.java b/src/main/java/com/tailbet/model/entity/RedPacket.java index bdc89ca..be0fd77 100644 --- a/src/main/java/com/tailbet/model/entity/RedPacket.java +++ b/src/main/java/com/tailbet/model/entity/RedPacket.java @@ -78,14 +78,14 @@ public class RedPacket { private Integer status; /** - * 预分配金额列表(JSON数组) + * 预分配金额列表(JSON数组),发红包时写入,领取时不再修改 */ private String shareAmounts; /** - * 已领取份额数 + * 下一个待领取份额的索引(从0开始,已领取N个则值为N) */ - private Integer shareTakenCount; + private Integer shareTakenIndex; /** * 创建时间 diff --git a/src/main/java/com/tailbet/service/IGameService.java b/src/main/java/com/tailbet/service/IGameService.java index ae345a1..8eacc4b 100644 --- a/src/main/java/com/tailbet/service/IGameService.java +++ b/src/main/java/com/tailbet/service/IGameService.java @@ -52,7 +52,10 @@ public interface IGameService { * 获取群当前可发红包的局(已封盘或正在发红包中) */ GameRound getActiveSendRound(Long groupId); - + /** + * 获取群当前结束下注的局 + */ + GameRound getBetEndRound(Long groupId); /** * 生成期号 */ diff --git a/src/main/java/com/tailbet/service/impl/GameServiceImpl.java b/src/main/java/com/tailbet/service/impl/GameServiceImpl.java index 2638b5e..849c5c3 100644 --- a/src/main/java/com/tailbet/service/impl/GameServiceImpl.java +++ b/src/main/java/com/tailbet/service/impl/GameServiceImpl.java @@ -92,6 +92,14 @@ public class GameServiceImpl implements IGameService { .last("LIMIT 1")); } + @Override + public GameRound getBetEndRound(Long groupId) { + return gameRoundMapper.selectOne(new LambdaQueryWrapper() + .eq(GameRound::getGroupId, groupId) + .eq(GameRound::getStatus, STATUS_BET_CLOSED) + .orderByDesc(GameRound::getCreateTime) + .last("LIMIT 1")); } + /** * 生成期号 */ @@ -184,13 +192,18 @@ public class GameServiceImpl implements IGameService { /** * 停止自动连开 + * 直接修改最新一局(不限状态)——autoNext 是"本局结算后是否自动开下一期"的开关, + * 即使本局已结算但下一期还未自动开出的窗口期内也要能关掉 */ @Override @Transactional public void stopAutoNext(Long groupId, Long userId) { - GameRound round = getOngoingRound(groupId); + GameRound round = gameRoundMapper.selectOne(new LambdaQueryWrapper() + .eq(GameRound::getGroupId, groupId) + .orderByDesc(GameRound::getCreateTime) + .last("LIMIT 1")); if (round == null) { - throw new RuntimeException("没有进行中的局"); + throw new RuntimeException("该群没有局"); } Group group = groupMapper.selectById(groupId); @@ -210,9 +223,9 @@ public class GameServiceImpl implements IGameService { @Override @Transactional public void setTargetDigit(Long groupId, Integer digit, Long userId) { - GameRound round = getBettingRound(groupId); + GameRound round = getBetEndRound(groupId); if (round == null) { - throw new RuntimeException("当前没有下注中的局"); + throw new RuntimeException("当前没有下注结束的局"); } round.setTargetDigit(digit); diff --git a/src/main/java/com/tailbet/service/impl/RedPacketServiceImpl.java b/src/main/java/com/tailbet/service/impl/RedPacketServiceImpl.java index c1ffc11..a33bf28 100644 --- a/src/main/java/com/tailbet/service/impl/RedPacketServiceImpl.java +++ b/src/main/java/com/tailbet/service/impl/RedPacketServiceImpl.java @@ -61,6 +61,13 @@ public class RedPacketServiceImpl implements IRedPacketService { throw new RuntimeException("用户不存在"); } + // 前置校验:总金额必须够每个包至少 1 分,避免产生 0 元红包 + BigDecimal minRequired = BigDecimal.valueOf(totalCount == null ? 0 : totalCount) + .multiply(new BigDecimal("0.01")); + if (totalAmount == null || totalAmount.compareTo(minRequired) < 0) { + throw new RuntimeException("红包总金额不能低于 " + minRequired.toPlainString() + " 元"); + } + // 扣积分 pointsService.subPoints(userId, totalAmount, IPointsService.TYPE_RP_SEND, null, null, "发送红包"); @@ -117,7 +124,7 @@ public class RedPacketServiceImpl implements IRedPacketService { log.error("序列化shareAmounts失败: {}", shareAmounts, e); throw new RuntimeException("系统错误"); } - rp.setShareTakenCount(0); + rp.setShareTakenIndex(0); redPacketMapper.updateById(rp); log.info("预分配红包份额: rpId={}, type={}, targetDigit={}, amounts={}", rp.getId(), rp.getType(), targetDigit, shareAmounts); @@ -164,14 +171,14 @@ public class RedPacketServiceImpl implements IRedPacketService { } // 更新手气王信息(领取金额最大的用户为手气王) - Integer takenCount = rp.getShareTakenCount(); + Integer takenIndex = rp.getShareTakenIndex(); BigDecimal currentLucky = rp.getLuckyAmount(); if (currentLucky == null || amount.compareTo(currentLucky) > 0) { rp.setLuckyAmount(amount); rp.setLuckyUserId(userId); rp.setLuckyDigit(amount.remainder(BigDecimal.TEN).intValue()); } - rp.setShareTakenCount(takenCount != null ? takenCount + 1 : 1); + rp.setShareTakenIndex(takenIndex != null ? takenIndex + 1 : 1); redPacketMapper.updateById(rp); @@ -288,9 +295,9 @@ public class RedPacketServiceImpl implements IRedPacketService { } /** - * 从预分配金额中顺序取值 - * 兼容旧数据:如果没有预分配金额,则走随机算法 - */ + * 从预分配金额中按索引顺序取值(不再修改 shareAmounts JSON) + * 兼容旧数据:如果没有预分配金额,则走随机算法 + */ private BigDecimal drawFromPreAllocatedAmounts(RedPacket rp) { if (rp.getShareAmounts() == null || rp.getShareAmounts().isEmpty()) { // 兼容旧数据fallback到随机 @@ -304,11 +311,13 @@ public class RedPacketServiceImpl implements IRedPacketService { if (amounts.isEmpty()) { return calculateReceiveAmount(rp); } - // 取第一个 - BigDecimal amount = amounts.remove(0); - // 更新JSON - rp.setShareAmounts(objectMapper.writeValueAsString(amounts)); - return amount; + // 按 shareTakenIndex 索引取值,shareAmounts 不再被修改 + Integer index = rp.getShareTakenIndex(); + if (index == null || index >= amounts.size()) { + // 越界:兼容旧数据或异常情况,走随机 + return calculateReceiveAmount(rp); + } + return amounts.get(index); } catch (Exception e) { log.error("解析shareAmounts失败: {}, error={}", rp.getShareAmounts(), e.getMessage()); return calculateReceiveAmount(rp); @@ -316,53 +325,61 @@ public class RedPacketServiceImpl implements IRedPacketService { } /** - * 预计算红包份额金额 - * targetDigit 非空时确保手气王金额尾数=targetDigit - * targetDigit 为空时按纯随机份额处理 - */ + * 预计算红包份额金额 + * targetDigit 非空时确保手气王金额尾数=targetDigit + * targetDigit 为空时按纯随机份额处理 + */ private List preCalculateShareAmounts(RedPacket rp, Integer targetDigit) { List amounts = new ArrayList<>(); int count = rp.getTotalCount(); BigDecimal total = rp.getTotalAmount(); + BigDecimal minUnit = new BigDecimal("0.01"); // 最小份额:1 分 if (count <= 1) { - // 只有1个,全部给手气王 amounts.add(total); return amounts; } Random random = new Random(); - // 计算手气王金额 - BigDecimal luckyAmount = calculateLuckyAmount(total, targetDigit, random); - // 确保手气王金额不超过总额 - if (luckyAmount.compareTo(total) > 0) { - luckyAmount = total; + // 1. 计算手气王金额(已保证不超过 luckyMax 且尾数=targetDigit) + BigDecimal luckyAmount = calculateLuckyAmount(total, targetDigit, count, random); + + // 2. 计算其他人总额(保证每人至少 minUnit) + BigDecimal minForOthers = minUnit.multiply(BigDecimal.valueOf(count - 1)); + BigDecimal remaining = total.subtract(luckyAmount); + if (remaining.compareTo(minForOthers) < 0) { + // 手气王挤占了他人最小份额,回收 + luckyAmount = total.subtract(minForOthers); + if (targetDigit != null) { + luckyAmount = adjustToTargetDigit(luckyAmount, luckyAmount.add(minUnit), targetDigit); + } + remaining = minForOthers; } - // 剩余金额平均分配给其他人 - BigDecimal remaining = total.subtract(luckyAmount); + // 3. 其他人金额(带小随机波动) BigDecimal avgAmount = remaining.divide(BigDecimal.valueOf(count - 1), 2, RoundingMode.HALF_UP); - - // 添加手气王金额 amounts.add(luckyAmount); - // 添加其他金额(略有随机波动) BigDecimal remainingForOthers = remaining; for (int i = 1; i < count - 1; i++) { - // 允许小范围波动 0.8~1.2 double factor = 0.8 + random.nextDouble() * 0.4; BigDecimal amount = avgAmount.multiply(BigDecimal.valueOf(factor)) .setScale(2, RoundingMode.HALF_UP); - // 确保不超过剩余 + // 下限保护:每个份额至少 minUnit + if (amount.compareTo(minUnit) < 0) { + amount = minUnit; + } + // 上限保护:不超过剩余 if (amount.compareTo(remainingForOthers) > 0) { amount = remainingForOthers; } amounts.add(amount); remainingForOthers = remainingForOthers.subtract(amount); } - // 最后一个拿剩余 - amounts.add(remainingForOthers.setScale(2, RoundingMode.HALF_UP)); + // 最后一个拿剩余(下限保护) + BigDecimal last = remainingForOthers.compareTo(minUnit) < 0 ? minUnit : remainingForOthers; + amounts.add(last.setScale(2, RoundingMode.HALF_UP)); // 打乱顺序(让领取顺序随机) Collections.shuffle(amounts); @@ -377,23 +394,57 @@ public class RedPacketServiceImpl implements IRedPacketService { /** * 计算手气王金额 - * 比例为总额的30%~50%,有目标尾数时确保尾数=targetDigit + * - 比例为总额的30%~50% + * - 有目标尾数时确保尾数=targetDigit + * - 不超过 luckyMax(total - minUnit * (count-1)),留出最小份额给其他人 */ - private BigDecimal calculateLuckyAmount(BigDecimal total, Integer targetDigit, Random random) { - // 手气王比例:总额的30%~50% - double ratio = 0.3 + random.nextDouble() * 0.2; - BigDecimal luckyAmountBase = total.multiply(BigDecimal.valueOf(ratio)); + private BigDecimal calculateLuckyAmount(BigDecimal total, Integer targetDigit, int totalCount, Random random) { + BigDecimal minUnit = new BigDecimal("0.01"); + BigDecimal minForOthers = minUnit.multiply(BigDecimal.valueOf(totalCount - 1)); + BigDecimal luckyMax = total.subtract(minForOthers); - // 未指定目标尾数时按基础金额返回 - if (targetDigit == null) { - return luckyAmountBase.setScale(2, RoundingMode.HALF_UP); + // 比例:总额的30%~50% + double ratio = 0.3 + random.nextDouble() * 0.2; + BigDecimal luckyAmount = total.multiply(BigDecimal.valueOf(ratio)) + .setScale(2, RoundingMode.HALF_UP); + + // 调整尾数(如果指定) + if (targetDigit != null) { + luckyAmount = adjustToTargetDigit(luckyAmount, luckyMax, targetDigit); } - // 指定了目标尾数,调整金额使尾数=targetDigit - int lastDigit = luckyAmountBase.remainder(BigDecimal.TEN).intValue(); - int diff = (targetDigit - lastDigit + 10) % 10; - return luckyAmountBase.add(BigDecimal.valueOf(diff)) - .setScale(2, RoundingMode.HALF_UP); + // 上限保护:不超过 luckyMax + if (luckyAmount.compareTo(luckyMax) > 0) { + luckyAmount = luckyMax; + if (targetDigit != null) { + luckyAmount = adjustToTargetDigit(luckyAmount, luckyMax, targetDigit); + } + } + + return luckyAmount; + } + + /** + * 调整金额让尾数=targetDigit,且不超过 maxAllowed + * 优先向上调整(保持金额更大),如会超出则向下调整 + */ + private BigDecimal adjustToTargetDigit(BigDecimal amount, BigDecimal maxAllowed, int targetDigit) { + BigDecimal minUnit = new BigDecimal("0.01"); + int lastDigit = amount.remainder(BigDecimal.TEN).intValue(); + if (lastDigit == targetDigit) { + return amount; + } + int upDiff = (targetDigit - lastDigit + 10) % 10; // 向上调 + int downDiff = (lastDigit - targetDigit + 10) % 10; // 向下调 + BigDecimal up = amount.add(BigDecimal.valueOf(upDiff)).setScale(2, RoundingMode.HALF_UP); + if (up.compareTo(maxAllowed) <= 0 && up.compareTo(minUnit) >= 0) { + return up; + } + BigDecimal down = amount.subtract(BigDecimal.valueOf(downDiff)).setScale(2, RoundingMode.HALF_UP); + if (down.compareTo(minUnit) >= 0) { + return down; + } + return amount; // 实在调不动,保持原值(兜底) } /** diff --git a/src/main/resources/sql/init.sql b/src/main/resources/sql/init.sql index 59b4618..d243e70 100644 --- a/src/main/resources/sql/init.sql +++ b/src/main/resources/sql/init.sql @@ -224,8 +224,8 @@ INSERT INTO sys_config (cfg_key, cfg_value, remark) VALUES INSERT INTO `guess_game`.`sys_config`(`id`, `cfg_key`, `cfg_value`, `remark`, `update_time`) VALUES (27, 'robot_id', '8', '机器人id', '2026-08-08 16:33:16'); -- red_packet 表增加预分配金额字段 -ALTER TABLE red_packet ADD COLUMN share_amounts JSON COMMENT '预分配金额列表(JSON数组)'; -ALTER TABLE red_packet ADD COLUMN share_taken_count INT DEFAULT 0 COMMENT '已领取份额数'; +ALTER TABLE red_packet m COLUMN share_amounts JSON COMMENT '预分配金额列表(JSON数组,发红包时写入,领取时不再修改)'; +ALTER TABLE red_packet ADD COLUMN share_taken_index INT DEFAULT 0 COMMENT '下一个待领取份额的索引(从0开始)'; -- game_round 表增加第一个红包ID字段 ALTER TABLE game_round ADD COLUMN first_rp_id BIGINT COMMENT '第一个红包ID';