发红包优化
This commit is contained in:
@@ -176,7 +176,7 @@ public class RedPacketServiceImpl implements IRedPacketService {
|
||||
if (currentLucky == null || amount.compareTo(currentLucky) > 0) {
|
||||
rp.setLuckyAmount(amount);
|
||||
rp.setLuckyUserId(userId);
|
||||
rp.setLuckyDigit(amount.remainder(BigDecimal.TEN).intValue());
|
||||
rp.setLuckyDigit(digitOf(amount));
|
||||
}
|
||||
rp.setShareTakenIndex(takenIndex != null ? takenIndex + 1 : 1);
|
||||
|
||||
@@ -358,7 +358,31 @@ public class RedPacketServiceImpl implements IRedPacketService {
|
||||
}
|
||||
|
||||
// 3. 其他人金额(带小随机波动)
|
||||
// 计算 avgAmount 后,确保 luckyAmount ≥ avgAmount × 1.2(maxFactor)
|
||||
// 这样循环里的 amount 自然 ≤ luckyAmount,无需钳制,金额严格守恒
|
||||
double maxFactor = 1.2;
|
||||
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);
|
||||
}
|
||||
// 若抬高后超过 luckyMax(total - 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);
|
||||
|
||||
BigDecimal remainingForOthers = remaining;
|
||||
@@ -370,15 +394,19 @@ public class RedPacketServiceImpl implements IRedPacketService {
|
||||
if (amount.compareTo(minUnit) < 0) {
|
||||
amount = minUnit;
|
||||
}
|
||||
// 上限保护:不超过剩余
|
||||
// 累加上限:不超过 remainingForOthers
|
||||
if (amount.compareTo(remainingForOthers) > 0) {
|
||||
amount = remainingForOthers;
|
||||
}
|
||||
amounts.add(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));
|
||||
|
||||
// 打乱顺序(让领取顺序随机)
|
||||
@@ -394,8 +422,9 @@ public class RedPacketServiceImpl implements IRedPacketService {
|
||||
|
||||
/**
|
||||
* 计算手气王金额
|
||||
* - 比例为总额的30%~50%
|
||||
* - 有目标尾数时确保尾数=targetDigit
|
||||
* - 比例为总额的 [1/count, 50%],下限定为 1/count 以保证 luckyAmount ≥ avgAmount
|
||||
* (手气王必须 ≥ 其他人的平均金额,否则随机波动后有人会比手气王还大)
|
||||
* - 有目标尾数时确保"分位尾数"=targetDigit
|
||||
* - 不超过 luckyMax(total - minUnit * (count-1)),留出最小份额给其他人
|
||||
*/
|
||||
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 luckyMax = total.subtract(minForOthers);
|
||||
|
||||
// 比例:总额的30%~50%
|
||||
double ratio = 0.3 + random.nextDouble() * 0.2;
|
||||
// 比例下限 1/count(保证 luckyAmount ≥ avgAmount),上限 50%
|
||||
double minRatio = 1.0 / totalCount;
|
||||
double ratio = minRatio + random.nextDouble() * (0.5 - minRatio);
|
||||
BigDecimal luckyAmount = total.multiply(BigDecimal.valueOf(ratio))
|
||||
.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) {
|
||||
BigDecimal minUnit = new BigDecimal("0.01");
|
||||
int lastDigit = amount.remainder(BigDecimal.TEN).intValue();
|
||||
int lastDigit = digitOf(amount);
|
||||
if (lastDigit == targetDigit) {
|
||||
return amount;
|
||||
}
|
||||
int upDiff = (targetDigit - lastDigit + 10) % 10; // 向上调
|
||||
int upDiff = (targetDigit - lastDigit + 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) {
|
||||
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) {
|
||||
return down;
|
||||
}
|
||||
@@ -448,22 +480,30 @@ public class RedPacketServiceImpl implements IRedPacketService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保份额列表中至少有1个金额的尾数=targetDigit
|
||||
* 若没有则从列表中找一个尾数匹配的金额,换到首位作为手气王
|
||||
* 取金额的"分位尾数":金额×100 后取个位(如 0.45→5,1.05→5,0.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) {
|
||||
for (BigDecimal amt : amounts) {
|
||||
if (amt.remainder(BigDecimal.TEN).intValue() == targetDigit) {
|
||||
if (digitOf(amounts.get(0)) == targetDigit) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 没有尾数匹配的,找一个换到首位
|
||||
// 首位不命中,找一个命中项 swap 到首位
|
||||
for (int i = 1; i < amounts.size(); i++) {
|
||||
BigDecimal amt = amounts.get(i);
|
||||
if (amt.remainder(BigDecimal.TEN).intValue() == targetDigit) {
|
||||
Collections.swap(amounts, i, 0);
|
||||
if (digitOf(amounts.get(i)) == targetDigit) {
|
||||
Collections.swap(amounts, 0, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 没有任何命中项,无法保证(这种情况很罕见:total/count 太小无空间调整)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user