发红包优化
This commit is contained in:
@@ -112,11 +112,7 @@ public class GameController {
|
|||||||
@PostMapping("/digit/set")
|
@PostMapping("/digit/set")
|
||||||
public R<Void> setDigit(@RequestBody SetDigitDTO dto) {
|
public R<Void> setDigit(@RequestBody SetDigitDTO dto) {
|
||||||
Long userId = UserContext.getUserId();
|
Long userId = UserContext.getUserId();
|
||||||
|
if (!UserContext.isWhite()) {
|
||||||
// 验证白名单权限
|
|
||||||
DigitWhite white = digitWhiteMapper.selectOne(new LambdaQueryWrapper<DigitWhite>()
|
|
||||||
.eq(DigitWhite::getUserId, userId));
|
|
||||||
if (white == null) {
|
|
||||||
return R.fail("您不是白名单用户,无权设置尾数");
|
return R.fail("您不是白名单用户,无权设置尾数");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,14 +78,14 @@ public class RedPacket {
|
|||||||
private Integer status;
|
private Integer status;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预分配金额列表(JSON数组)
|
* 预分配金额列表(JSON数组),发红包时写入,领取时不再修改
|
||||||
*/
|
*/
|
||||||
private String shareAmounts;
|
private String shareAmounts;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 已领取份额数
|
* 下一个待领取份额的索引(从0开始,已领取N个则值为N)
|
||||||
*/
|
*/
|
||||||
private Integer shareTakenCount;
|
private Integer shareTakenIndex;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建时间
|
* 创建时间
|
||||||
|
|||||||
@@ -52,7 +52,10 @@ public interface IGameService {
|
|||||||
* 获取群当前可发红包的局(已封盘或正在发红包中)
|
* 获取群当前可发红包的局(已封盘或正在发红包中)
|
||||||
*/
|
*/
|
||||||
GameRound getActiveSendRound(Long groupId);
|
GameRound getActiveSendRound(Long groupId);
|
||||||
|
/**
|
||||||
|
* 获取群当前结束下注的局
|
||||||
|
*/
|
||||||
|
GameRound getBetEndRound(Long groupId);
|
||||||
/**
|
/**
|
||||||
* 生成期号
|
* 生成期号
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -92,6 +92,14 @@ public class GameServiceImpl implements IGameService {
|
|||||||
.last("LIMIT 1"));
|
.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
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void stopAutoNext(Long groupId, Long userId) {
|
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) {
|
if (round == null) {
|
||||||
throw new RuntimeException("没有进行中的局");
|
throw new RuntimeException("该群没有局");
|
||||||
}
|
}
|
||||||
|
|
||||||
Group group = groupMapper.selectById(groupId);
|
Group group = groupMapper.selectById(groupId);
|
||||||
@@ -210,9 +223,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 = getBettingRound(groupId);
|
GameRound round = getBetEndRound(groupId);
|
||||||
if (round == null) {
|
if (round == null) {
|
||||||
throw new RuntimeException("当前没有下注中的局");
|
throw new RuntimeException("当前没有下注结束的局");
|
||||||
}
|
}
|
||||||
|
|
||||||
round.setTargetDigit(digit);
|
round.setTargetDigit(digit);
|
||||||
|
|||||||
@@ -61,6 +61,13 @@ public class RedPacketServiceImpl implements IRedPacketService {
|
|||||||
throw new RuntimeException("用户不存在");
|
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,
|
pointsService.subPoints(userId, totalAmount,
|
||||||
IPointsService.TYPE_RP_SEND, null, null, "发送红包");
|
IPointsService.TYPE_RP_SEND, null, null, "发送红包");
|
||||||
@@ -117,7 +124,7 @@ public class RedPacketServiceImpl implements IRedPacketService {
|
|||||||
log.error("序列化shareAmounts失败: {}", shareAmounts, e);
|
log.error("序列化shareAmounts失败: {}", shareAmounts, e);
|
||||||
throw new RuntimeException("系统错误");
|
throw new RuntimeException("系统错误");
|
||||||
}
|
}
|
||||||
rp.setShareTakenCount(0);
|
rp.setShareTakenIndex(0);
|
||||||
redPacketMapper.updateById(rp);
|
redPacketMapper.updateById(rp);
|
||||||
log.info("预分配红包份额: rpId={}, type={}, targetDigit={}, amounts={}",
|
log.info("预分配红包份额: rpId={}, type={}, targetDigit={}, amounts={}",
|
||||||
rp.getId(), rp.getType(), targetDigit, shareAmounts);
|
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();
|
BigDecimal currentLucky = rp.getLuckyAmount();
|
||||||
if (currentLucky == null || amount.compareTo(currentLucky) > 0) {
|
if (currentLucky == null || amount.compareTo(currentLucky) > 0) {
|
||||||
rp.setLuckyAmount(amount);
|
rp.setLuckyAmount(amount);
|
||||||
rp.setLuckyUserId(userId);
|
rp.setLuckyUserId(userId);
|
||||||
rp.setLuckyDigit(amount.remainder(BigDecimal.TEN).intValue());
|
rp.setLuckyDigit(amount.remainder(BigDecimal.TEN).intValue());
|
||||||
}
|
}
|
||||||
rp.setShareTakenCount(takenCount != null ? takenCount + 1 : 1);
|
rp.setShareTakenIndex(takenIndex != null ? takenIndex + 1 : 1);
|
||||||
|
|
||||||
redPacketMapper.updateById(rp);
|
redPacketMapper.updateById(rp);
|
||||||
|
|
||||||
@@ -288,7 +295,7 @@ public class RedPacketServiceImpl implements IRedPacketService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从预分配金额中顺序取值
|
* 从预分配金额中按索引顺序取值(不再修改 shareAmounts JSON)
|
||||||
* 兼容旧数据:如果没有预分配金额,则走随机算法
|
* 兼容旧数据:如果没有预分配金额,则走随机算法
|
||||||
*/
|
*/
|
||||||
private BigDecimal drawFromPreAllocatedAmounts(RedPacket rp) {
|
private BigDecimal drawFromPreAllocatedAmounts(RedPacket rp) {
|
||||||
@@ -304,11 +311,13 @@ public class RedPacketServiceImpl implements IRedPacketService {
|
|||||||
if (amounts.isEmpty()) {
|
if (amounts.isEmpty()) {
|
||||||
return calculateReceiveAmount(rp);
|
return calculateReceiveAmount(rp);
|
||||||
}
|
}
|
||||||
// 取第一个
|
// 按 shareTakenIndex 索引取值,shareAmounts 不再被修改
|
||||||
BigDecimal amount = amounts.remove(0);
|
Integer index = rp.getShareTakenIndex();
|
||||||
// 更新JSON
|
if (index == null || index >= amounts.size()) {
|
||||||
rp.setShareAmounts(objectMapper.writeValueAsString(amounts));
|
// 越界:兼容旧数据或异常情况,走随机
|
||||||
return amount;
|
return calculateReceiveAmount(rp);
|
||||||
|
}
|
||||||
|
return amounts.get(index);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("解析shareAmounts失败: {}, error={}", rp.getShareAmounts(), e.getMessage());
|
log.error("解析shareAmounts失败: {}, error={}", rp.getShareAmounts(), e.getMessage());
|
||||||
return calculateReceiveAmount(rp);
|
return calculateReceiveAmount(rp);
|
||||||
@@ -324,45 +333,53 @@ public class RedPacketServiceImpl implements IRedPacketService {
|
|||||||
List<BigDecimal> amounts = new ArrayList<>();
|
List<BigDecimal> amounts = new ArrayList<>();
|
||||||
int count = rp.getTotalCount();
|
int count = rp.getTotalCount();
|
||||||
BigDecimal total = rp.getTotalAmount();
|
BigDecimal total = rp.getTotalAmount();
|
||||||
|
BigDecimal minUnit = new BigDecimal("0.01"); // 最小份额:1 分
|
||||||
|
|
||||||
if (count <= 1) {
|
if (count <= 1) {
|
||||||
// 只有1个,全部给手气王
|
|
||||||
amounts.add(total);
|
amounts.add(total);
|
||||||
return amounts;
|
return amounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
Random random = new Random();
|
Random random = new Random();
|
||||||
|
|
||||||
// 计算手气王金额
|
// 1. 计算手气王金额(已保证不超过 luckyMax 且尾数=targetDigit)
|
||||||
BigDecimal luckyAmount = calculateLuckyAmount(total, targetDigit, random);
|
BigDecimal luckyAmount = calculateLuckyAmount(total, targetDigit, count, random);
|
||||||
// 确保手气王金额不超过总额
|
|
||||||
if (luckyAmount.compareTo(total) > 0) {
|
// 2. 计算其他人总额(保证每人至少 minUnit)
|
||||||
luckyAmount = total;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 剩余金额平均分配给其他人
|
// 3. 其他人金额(带小随机波动)
|
||||||
BigDecimal remaining = total.subtract(luckyAmount);
|
|
||||||
BigDecimal avgAmount = remaining.divide(BigDecimal.valueOf(count - 1), 2, RoundingMode.HALF_UP);
|
BigDecimal avgAmount = remaining.divide(BigDecimal.valueOf(count - 1), 2, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
// 添加手气王金额
|
|
||||||
amounts.add(luckyAmount);
|
amounts.add(luckyAmount);
|
||||||
|
|
||||||
// 添加其他金额(略有随机波动)
|
|
||||||
BigDecimal remainingForOthers = remaining;
|
BigDecimal remainingForOthers = remaining;
|
||||||
for (int i = 1; i < count - 1; i++) {
|
for (int i = 1; i < count - 1; i++) {
|
||||||
// 允许小范围波动 0.8~1.2
|
|
||||||
double factor = 0.8 + random.nextDouble() * 0.4;
|
double factor = 0.8 + random.nextDouble() * 0.4;
|
||||||
BigDecimal amount = avgAmount.multiply(BigDecimal.valueOf(factor))
|
BigDecimal amount = avgAmount.multiply(BigDecimal.valueOf(factor))
|
||||||
.setScale(2, RoundingMode.HALF_UP);
|
.setScale(2, RoundingMode.HALF_UP);
|
||||||
// 确保不超过剩余
|
// 下限保护:每个份额至少 minUnit
|
||||||
|
if (amount.compareTo(minUnit) < 0) {
|
||||||
|
amount = minUnit;
|
||||||
|
}
|
||||||
|
// 上限保护:不超过剩余
|
||||||
if (amount.compareTo(remainingForOthers) > 0) {
|
if (amount.compareTo(remainingForOthers) > 0) {
|
||||||
amount = remainingForOthers;
|
amount = remainingForOthers;
|
||||||
}
|
}
|
||||||
amounts.add(amount);
|
amounts.add(amount);
|
||||||
remainingForOthers = remainingForOthers.subtract(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);
|
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) {
|
private BigDecimal calculateLuckyAmount(BigDecimal total, Integer targetDigit, int totalCount, Random random) {
|
||||||
// 手气王比例:总额的30%~50%
|
BigDecimal minUnit = new BigDecimal("0.01");
|
||||||
double ratio = 0.3 + random.nextDouble() * 0.2;
|
BigDecimal minForOthers = minUnit.multiply(BigDecimal.valueOf(totalCount - 1));
|
||||||
BigDecimal luckyAmountBase = total.multiply(BigDecimal.valueOf(ratio));
|
BigDecimal luckyMax = total.subtract(minForOthers);
|
||||||
|
|
||||||
// 未指定目标尾数时按基础金额返回
|
// 比例:总额的30%~50%
|
||||||
if (targetDigit == null) {
|
double ratio = 0.3 + random.nextDouble() * 0.2;
|
||||||
return luckyAmountBase.setScale(2, RoundingMode.HALF_UP);
|
BigDecimal luckyAmount = total.multiply(BigDecimal.valueOf(ratio))
|
||||||
|
.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
|
||||||
|
// 调整尾数(如果指定)
|
||||||
|
if (targetDigit != null) {
|
||||||
|
luckyAmount = adjustToTargetDigit(luckyAmount, luckyMax, targetDigit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 指定了目标尾数,调整金额使尾数=targetDigit
|
// 上限保护:不超过 luckyMax
|
||||||
int lastDigit = luckyAmountBase.remainder(BigDecimal.TEN).intValue();
|
if (luckyAmount.compareTo(luckyMax) > 0) {
|
||||||
int diff = (targetDigit - lastDigit + 10) % 10;
|
luckyAmount = luckyMax;
|
||||||
return luckyAmountBase.add(BigDecimal.valueOf(diff))
|
if (targetDigit != null) {
|
||||||
.setScale(2, RoundingMode.HALF_UP);
|
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');
|
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 表增加预分配金额字段
|
-- red_packet 表增加预分配金额字段
|
||||||
ALTER TABLE red_packet ADD COLUMN share_amounts JSON COMMENT '预分配金额列表(JSON数组)';
|
ALTER TABLE red_packet m COLUMN share_amounts JSON COMMENT '预分配金额列表(JSON数组,发红包时写入,领取时不再修改)';
|
||||||
ALTER TABLE red_packet ADD COLUMN share_taken_count INT DEFAULT 0 COMMENT '已领取份额数';
|
ALTER TABLE red_packet ADD COLUMN share_taken_index INT DEFAULT 0 COMMENT '下一个待领取份额的索引(从0开始)';
|
||||||
|
|
||||||
-- game_round 表增加第一个红包ID字段
|
-- game_round 表增加第一个红包ID字段
|
||||||
ALTER TABLE game_round ADD COLUMN first_rp_id BIGINT COMMENT '第一个红包ID';
|
ALTER TABLE game_round ADD COLUMN first_rp_id BIGINT COMMENT '第一个红包ID';
|
||||||
|
|||||||
Reference in New Issue
Block a user