diff --git a/src/main/java/com/tailbet/service/impl/RedPacketServiceImpl.java b/src/main/java/com/tailbet/service/impl/RedPacketServiceImpl.java index a33bf28..22ef5bf 100644 --- a/src/main/java/com/tailbet/service/impl/RedPacketServiceImpl.java +++ b/src/main/java/com/tailbet/service/impl/RedPacketServiceImpl.java @@ -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 amounts, int targetDigit) { - for (BigDecimal amt : amounts) { - if (amt.remainder(BigDecimal.TEN).intValue() == targetDigit) { - return; - } + 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 太小无空间调整) } }