发红包优化
This commit is contained in:
@@ -112,11 +112,7 @@ public class GameController {
|
||||
@PostMapping("/digit/set")
|
||||
public R<Void> setDigit(@RequestBody SetDigitDTO dto) {
|
||||
Long userId = UserContext.getUserId();
|
||||
|
||||
// 验证白名单权限
|
||||
DigitWhite white = digitWhiteMapper.selectOne(new LambdaQueryWrapper<DigitWhite>()
|
||||
.eq(DigitWhite::getUserId, userId));
|
||||
if (white == null) {
|
||||
if (!UserContext.isWhite()) {
|
||||
return R.fail("您不是白名单用户,无权设置尾数");
|
||||
}
|
||||
|
||||
|
||||
@@ -78,14 +78,14 @@ public class RedPacket {
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 预分配金额列表(JSON数组)
|
||||
* 预分配金额列表(JSON数组),发红包时写入,领取时不再修改
|
||||
*/
|
||||
private String shareAmounts;
|
||||
|
||||
/**
|
||||
* 已领取份额数
|
||||
* 下一个待领取份额的索引(从0开始,已领取N个则值为N)
|
||||
*/
|
||||
private Integer shareTakenCount;
|
||||
private Integer shareTakenIndex;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
|
||||
@@ -52,7 +52,10 @@ public interface IGameService {
|
||||
* 获取群当前可发红包的局(已封盘或正在发红包中)
|
||||
*/
|
||||
GameRound getActiveSendRound(Long groupId);
|
||||
|
||||
/**
|
||||
* 获取群当前结束下注的局
|
||||
*/
|
||||
GameRound getBetEndRound(Long groupId);
|
||||
/**
|
||||
* 生成期号
|
||||
*/
|
||||
|
||||
@@ -92,6 +92,14 @@ public class GameServiceImpl implements IGameService {
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameRound getBetEndRound(Long groupId) {
|
||||
return gameRoundMapper.selectOne(new LambdaQueryWrapper<GameRound>()
|
||||
.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<GameRound>()
|
||||
.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);
|
||||
|
||||
@@ -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<BigDecimal> preCalculateShareAmounts(RedPacket rp, Integer targetDigit) {
|
||||
List<BigDecimal> 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; // 实在调不动,保持原值(兜底)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user