发红包优化

This commit is contained in:
wells
2026-08-09 14:24:59 +08:00
parent c5250e23ef
commit 6be2339d87
6 changed files with 121 additions and 58 deletions
@@ -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
* - 不超过 luckyMaxtotal - 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; // 实在调不动,保持原值(兜底)
}
/**