发红包优化

This commit is contained in:
wells
2026-08-09 14:52:22 +08:00
parent 6be2339d87
commit aad68e407e
@@ -176,7 +176,7 @@ public class RedPacketServiceImpl implements IRedPacketService {
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(digitOf(amount));
} }
rp.setShareTakenIndex(takenIndex != null ? takenIndex + 1 : 1); rp.setShareTakenIndex(takenIndex != null ? takenIndex + 1 : 1);
@@ -358,7 +358,31 @@ public class RedPacketServiceImpl implements IRedPacketService {
} }
// 3. 其他人金额(带小随机波动) // 3. 其他人金额(带小随机波动)
// 计算 avgAmount 后,确保 luckyAmount ≥ avgAmount × 1.2maxFactor
// 这样循环里的 amount 自然 ≤ luckyAmount,无需钳制,金额严格守恒
double maxFactor = 1.2;
BigDecimal avgAmount = remaining.divide(BigDecimal.valueOf(count - 1), 2, RoundingMode.HALF_UP); BigDecimal avgAmount = remaining.divide(BigDecimal.valueOf(count - 1), 2, RoundingMode.HALF_UP);
BigDecimal needed = avgAmount.multiply(BigDecimal.valueOf(maxFactor))
.setScale(2, RoundingMode.HALF_UP);
if (luckyAmount.compareTo(needed) < 0) {
// 手气王不够大,抬高到 needed(满足"分位尾数"=targetDigit 优先)
BigDecimal lifted = needed;
if (targetDigit != null) {
lifted = adjustToTargetDigit(lifted, total, targetDigit);
}
// 若抬高后超过 luckyMaxtotal - minForOthers),回退取 luckyMax 并再调尾数
BigDecimal cap = total.subtract(minForOthers);
if (lifted.compareTo(cap) > 0) {
lifted = cap;
if (targetDigit != null) {
lifted = adjustToTargetDigit(lifted, cap, targetDigit);
}
}
luckyAmount = lifted;
remaining = total.subtract(luckyAmount);
// 重新计算 avgAmount,确保循环里的金额仍 ≥ minUnit
avgAmount = remaining.divide(BigDecimal.valueOf(count - 1), 2, RoundingMode.HALF_UP);
}
amounts.add(luckyAmount); amounts.add(luckyAmount);
BigDecimal remainingForOthers = remaining; BigDecimal remainingForOthers = remaining;
@@ -370,15 +394,19 @@ public class RedPacketServiceImpl implements IRedPacketService {
if (amount.compareTo(minUnit) < 0) { if (amount.compareTo(minUnit) < 0) {
amount = minUnit; amount = minUnit;
} }
// 上限保护:不超过剩余 // 累加上限:不超过 remainingForOthers
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);
} }
// 最后一个拿剩余(下限保护) // 最后一个拿剩余
BigDecimal last = remainingForOthers.compareTo(minUnit) < 0 ? minUnit : remainingForOthers; BigDecimal last = remainingForOthers;
// 下限保护:累减后可能 < minUnit
if (last.compareTo(minUnit) < 0) {
last = minUnit;
}
amounts.add(last.setScale(2, RoundingMode.HALF_UP)); amounts.add(last.setScale(2, RoundingMode.HALF_UP));
// 打乱顺序(让领取顺序随机) // 打乱顺序(让领取顺序随机)
@@ -394,8 +422,9 @@ public class RedPacketServiceImpl implements IRedPacketService {
/** /**
* 计算手气王金额 * 计算手气王金额
* - 比例为总额的30%~50% * - 比例为总额的 [1/count, 50%],下限定为 1/count 以保证 luckyAmount ≥ avgAmount
* - 有目标尾数时确保尾数=targetDigit * (手气王必须 ≥ 其他人的平均金额,否则随机波动后有人会比手气王还大)
* - 有目标尾数时确保"分位尾数"=targetDigit
* - 不超过 luckyMaxtotal - minUnit * (count-1)),留出最小份额给其他人 * - 不超过 luckyMaxtotal - minUnit * (count-1)),留出最小份额给其他人
*/ */
private BigDecimal calculateLuckyAmount(BigDecimal total, Integer targetDigit, int totalCount, Random random) { private BigDecimal calculateLuckyAmount(BigDecimal total, Integer targetDigit, int totalCount, Random random) {
@@ -403,8 +432,9 @@ public class RedPacketServiceImpl implements IRedPacketService {
BigDecimal minForOthers = minUnit.multiply(BigDecimal.valueOf(totalCount - 1)); BigDecimal minForOthers = minUnit.multiply(BigDecimal.valueOf(totalCount - 1));
BigDecimal luckyMax = total.subtract(minForOthers); BigDecimal luckyMax = total.subtract(minForOthers);
// 比例:总额的30%~50% // 比例下限 1/count(保证 luckyAmount ≥ avgAmount),上限 50%
double ratio = 0.3 + random.nextDouble() * 0.2; double minRatio = 1.0 / totalCount;
double ratio = minRatio + random.nextDouble() * (0.5 - minRatio);
BigDecimal luckyAmount = total.multiply(BigDecimal.valueOf(ratio)) BigDecimal luckyAmount = total.multiply(BigDecimal.valueOf(ratio))
.setScale(2, RoundingMode.HALF_UP); .setScale(2, RoundingMode.HALF_UP);
@@ -425,22 +455,24 @@ public class RedPacketServiceImpl implements IRedPacketService {
} }
/** /**
* 调整金额让尾数=targetDigit,且不超过 maxAllowed * 调整金额让"分位尾数"=targetDigit,且不超过 maxAllowed
* 优先向上调整(保持金额更大),如会超出则向下调整 * 优先向上调整(保持金额更大),如会超出则向下调整
*/ */
private BigDecimal adjustToTargetDigit(BigDecimal amount, BigDecimal maxAllowed, int targetDigit) { private BigDecimal adjustToTargetDigit(BigDecimal amount, BigDecimal maxAllowed, int targetDigit) {
BigDecimal minUnit = new BigDecimal("0.01"); BigDecimal minUnit = new BigDecimal("0.01");
int lastDigit = amount.remainder(BigDecimal.TEN).intValue(); int lastDigit = digitOf(amount);
if (lastDigit == targetDigit) { if (lastDigit == targetDigit) {
return amount; return amount;
} }
int upDiff = (targetDigit - lastDigit + 10) % 10; // 向上调 int upDiff = (targetDigit - lastDigit + 10) % 10; // 向上调(单位:分)
int downDiff = (lastDigit - targetDigit + 10) % 10; // 向下调 int downDiff = (lastDigit - targetDigit + 10) % 10; // 向下调
BigDecimal up = amount.add(BigDecimal.valueOf(upDiff)).setScale(2, RoundingMode.HALF_UP); BigDecimal up = amount.add(BigDecimal.valueOf(upDiff).movePointLeft(2))
.setScale(2, RoundingMode.HALF_UP);
if (up.compareTo(maxAllowed) <= 0 && up.compareTo(minUnit) >= 0) { if (up.compareTo(maxAllowed) <= 0 && up.compareTo(minUnit) >= 0) {
return up; return up;
} }
BigDecimal down = amount.subtract(BigDecimal.valueOf(downDiff)).setScale(2, RoundingMode.HALF_UP); BigDecimal down = amount.subtract(BigDecimal.valueOf(downDiff).movePointLeft(2))
.setScale(2, RoundingMode.HALF_UP);
if (down.compareTo(minUnit) >= 0) { if (down.compareTo(minUnit) >= 0) {
return down; return down;
} }
@@ -448,22 +480,30 @@ public class RedPacketServiceImpl implements IRedPacketService {
} }
/** /**
* 确保份额列表中至少有1个金额的尾数=targetDigit * 取金额的"分位尾数":金额×100 后取个位(如 0.45→51.05→50.40→0
* 若没有则从列表中找一个尾数匹配的金额,换到首位作为手气王 * BigDecimal.remainder(BigDecimal.TEN) 对小数返回自身+intValue 截断会丢精度,
* 这里按国内红包/微信红包标准按"分位"算尾数
*/ */
private static int digitOf(BigDecimal amount) {
if (amount == null) return 0;
return amount.movePointRight(2).remainder(BigDecimal.TEN).intValue();
}
/**
* 确保 amounts[0] 的"分位尾数"=targetDigit(手气王必须在首位)
* 如果首位已经命中则不操作;否则找一个命中项 swap 到首位;都没有则兜底不动
*/
private void ensureLuckyDigit(List<BigDecimal> amounts, int targetDigit) { private void ensureLuckyDigit(List<BigDecimal> amounts, int targetDigit) {
for (BigDecimal amt : amounts) { if (digitOf(amounts.get(0)) == targetDigit) {
if (amt.remainder(BigDecimal.TEN).intValue() == targetDigit) { return;
return;
}
} }
// 没有尾数匹配的,找一个换到首位 // 首位不命中,找一个命中项 swap 到首位
for (int i = 1; i < amounts.size(); i++) { for (int i = 1; i < amounts.size(); i++) {
BigDecimal amt = amounts.get(i); if (digitOf(amounts.get(i)) == targetDigit) {
if (amt.remainder(BigDecimal.TEN).intValue() == targetDigit) { Collections.swap(amounts, 0, i);
Collections.swap(amounts, i, 0);
return; return;
} }
} }
// 没有任何命中项,无法保证(这种情况很罕见:total/count 太小无空间调整)
} }
} }