diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/LotteryMultiDrawExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/LotteryMultiDrawExe.java index 44e5c09f..e7ef57ba 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/LotteryMultiDrawExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/activity/LotteryMultiDrawExe.java @@ -21,7 +21,11 @@ import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketRec import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketService; import com.red.circle.other.infra.database.rds.service.activity.LotteryUserCountService; import com.red.circle.other.infra.database.rds.service.activity.LotteryUserProgressService; +import com.red.circle.other.infra.database.rds.service.activity.LotteryDeviceRecordService; +import com.red.circle.other.infra.database.rds.service.activity.UserActivityRechargeService; +import com.red.circle.other.infra.database.rds.service.user.device.LatestMobileDeviceService; import com.red.circle.other.infra.utils.ZonedDateTimeUtils; +import com.red.circle.other.inner.asserts.OtherErrorCode; import com.red.circle.other.inner.asserts.lottery.LotteryErrorCode; import com.red.circle.tool.core.date.TimestampUtils; import java.math.BigDecimal; @@ -59,6 +63,9 @@ public class LotteryMultiDrawExe { private final LotteryUserProgressService lotteryUserProgressService; private final LotteryPrizeGrantService lotteryPrizeGrantService; private final RedisService redisService; + private final LatestMobileDeviceService latestMobileDeviceService; + private final LotteryDeviceRecordService lotteryDeviceRecordService; + private final UserActivityRechargeService userActivityRechargeService; @Transactional(rollbackFor = Exception.class) public LotteryMultiDrawResultCO execute(LotteryMultiDrawCmd cmd) { @@ -70,27 +77,33 @@ public class LotteryMultiDrawExe { // 1. 校验活动 LotteryActivity activity = validateActivity(activityId, now); - // 2. 校验是否启用连抽 + // 2. 校验设备唯一性(防止小号薅羊毛) + checkDeviceUnique(activityId, userId); + + // 3. 校验是否启用连抽 ResponseAssert.isTrue(LotteryErrorCode.MULTI_DRAW_NOT_ENABLED, activity.getEnableMultiDraw() != null && activity.getEnableMultiDraw() == 1); - // 3. 校验抽奖次数限制(需要剩余足够次数) + // 4. 查询用户充值金额(用于过滤美元奖励) + BigDecimal userRechargeAmount = getUserRechargeAmount(userId, activityId); + + // 5. 校验抽奖次数限制(需要剩余足够次数) validateDrawCount(userId, activityId, activity, drawCount); - // 4. 校验并扣除抽奖券(连抽消耗更少) + // 6. 校验并扣除抽奖券(连抽消耗更少) Integer ticketCost = activity.getMultiDrawTicketCost() != null ? activity.getMultiDrawTicketCost() : drawCount * activity.getTicketCost(); deductTicket(userId, ticketCost); - // 5. 获取或创建用户进度(用于动态概率) + // 7. 获取或创建用户进度(用于动态概率) LotteryUserProgress progress = getOrCreateUserProgress(userId, activityId); - // 6. 生成批次号 + // 8. 生成批次号 String batchNo = generateBatchNo(); - // 7. 执行连抽 + // 9. 执行连抽 List results = new ArrayList<>(); boolean guaranteeTriggered = false; BigDecimal batchTotalAmount = BigDecimal.ZERO; @@ -101,7 +114,7 @@ public class LotteryMultiDrawExe { LotteryUserProgress currentProgress = lotteryUserProgressService.getById(progress.getId()); // 使用动态概率抽奖 - LotteryPrize prize = drawPrizeWithDynamicProbability(activityId, activity, currentProgress); + LotteryPrize prize = drawPrizeWithDynamicProbability(activityId, activity, currentProgress, userRechargeAmount); // 扣减库存 if (prize != null) { @@ -138,7 +151,7 @@ public class LotteryMultiDrawExe { } } - // 8. 检查连抽保底 + // 10. 检查连抽保底 BigDecimal guaranteeMinAmount = activity.getMultiDrawGuaranteeMinAmount() != null ? activity.getMultiDrawGuaranteeMinAmount() : BigDecimal.ZERO; @@ -151,13 +164,64 @@ public class LotteryMultiDrawExe { userId, activityId, batchTotalAmount); } - // 9. 更新用户抽奖次数(+drawCount次) + // 11. 更新用户抽奖次数(+drawCount次) updateUserDrawCount(userId, activityId, drawCount); - // 10. 组装返回结果 + // 12. 记录设备信息(首次抽奖时) + recordDeviceAfterDraw(activityId, userId); + + // 13. 组装返回结果 return buildMultiDrawResult(batchNo, results, guaranteeTriggered); } + /** + * 校验设备唯一性(防止小号薅羊毛) + */ + private void checkDeviceUnique(Long activityId, Long userId) { + String fingerprint = latestMobileDeviceService.getEnhancedDeviceFingerprintByUserId(userId); + + if (com.red.circle.tool.core.text.StringUtils.isBlank(fingerprint)) { + log.warn("Failed to get device fingerprint in multi-draw, userId: {}", userId); + return; + } + + com.red.circle.other.infra.database.rds.entity.activity.LotteryDeviceRecord existingRecord = lotteryDeviceRecordService + .getByActivityAndFingerprint(activityId, fingerprint); + + if (existingRecord != null && !existingRecord.getUserId().equals(userId)) { + log.warn("Duplicate device detected in multi-draw, activityId: {}, currentUser: {}, boundUser: {}, fingerprint: {}", + activityId, userId, existingRecord.getUserId(), fingerprint); + ResponseAssert.isTrue(OtherErrorCode.DEVICE_ALREADY_USED, false); + } + } + + /** + * 记录设备信息(抽奖成功后) + */ + private void recordDeviceAfterDraw(Long activityId, Long userId) { + try { + String fingerprint = latestMobileDeviceService.getEnhancedDeviceFingerprintByUserId(userId); + if (com.red.circle.tool.core.text.StringUtils.isNotBlank(fingerprint)) { + lotteryDeviceRecordService.recordDevice(activityId, userId, fingerprint); + } + } catch (Exception e) { + log.error("Failed to record device in multi-draw, activityId: {}, userId: {}", activityId, userId, e); + } + } + + /** + * 查询用户充值金额 + */ + private BigDecimal getUserRechargeAmount(Long userId, Long activityId) { + try { + BigDecimal amount = userActivityRechargeService.getUserTotalAmount(userId, activityId); + return amount != null ? amount : BigDecimal.ZERO; + } catch (Exception e) { + log.error("Failed to get user recharge amount in multi-draw, userId: {}, activityId: {}", userId, activityId, e); + return BigDecimal.ZERO; + } + } + private LotteryActivity validateActivity(Long activityId, Timestamp now) { LotteryActivity activity = lotteryActivityService.getById(activityId); ResponseAssert.isTrue(LotteryErrorCode.ACTIVITY_NOT_FOUND, activity != null); @@ -203,9 +267,10 @@ public class LotteryMultiDrawExe { return progress; } - private LotteryPrize drawPrizeWithDynamicProbability(Long activityId, LotteryActivity activity, LotteryUserProgress progress) { + private LotteryPrize drawPrizeWithDynamicProbability(Long activityId, LotteryActivity activity, + LotteryUserProgress progress, BigDecimal userRechargeAmount) { if (activity.getEnableDynamicProbability() == null || activity.getEnableDynamicProbability() != 1) { - return drawPrizeWithMultiProbability(activityId, progress.getUserId()); + return drawPrizeWithMultiProbability(activityId, progress.getUserId(), userRechargeAmount); } String stage = determinePrizePool(progress, activity); log.debug("Multi-draw stage: {}, userId: {}, drawCount: {}, totalAmount: {}", @@ -215,6 +280,9 @@ public class LotteryMultiDrawExe { // 过滤用户已中过的BADGE类型奖品 prizes = filterAlreadyWonBadgePrizes(prizes, progress.getUserId()); + // 过滤美元奖励(充值金额为0的用户不能抽到) + prizes = filterMoneyPrizes(prizes, userRechargeAmount); + prizes = filterOverThresholdPrizes(prizes, progress, activity.getWithdrawThreshold()); if (prizes.isEmpty()) { return null; @@ -316,7 +384,7 @@ public class LotteryMultiDrawExe { return prizes.get(prizes.size() - 1); } - private LotteryPrize drawPrizeWithMultiProbability(Long activityId, Long userId) { + private LotteryPrize drawPrizeWithMultiProbability(Long activityId, Long userId, BigDecimal userRechargeAmount) { List prizes = lotteryPrizeService.query() .eq(LotteryPrize::getActivityId, activityId) .gt(LotteryPrize::getRemainingStock, 0) @@ -329,6 +397,9 @@ public class LotteryMultiDrawExe { // 过滤用户已中过的BADGE类型奖品 prizes = filterAlreadyWonBadgePrizes(prizes, userId); + // 过滤美元奖励(充值金额为0的用户不能抽到) + prizes = filterMoneyPrizes(prizes, userRechargeAmount); + if (prizes.isEmpty()) { return null; } @@ -554,6 +625,37 @@ public class LotteryMultiDrawExe { .collect(Collectors.toList()); } + /** + * 过滤美元奖励(充值金额为0的用户不能抽到) + */ + private List filterMoneyPrizes(List prizes, BigDecimal userRechargeAmount) { + if (prizes == null || prizes.isEmpty()) { + return prizes; + } + + // 如果用户充值金额>0,不需要过滤 + if (userRechargeAmount.compareTo(BigDecimal.ZERO) > 0) { + return prizes; + } + + // 充值金额为0,过滤掉MONEY类型奖品 + List filtered = prizes.stream() + .filter(prize -> { + if ("MONEY".equals(prize.getPrizeType())) { + log.info("User has no recharge in multi-draw, filtering MONEY prize. userRechargeAmount: {}, prizeId: {}, prizeName: {}", + userRechargeAmount, prize.getId(), prize.getPrizeName()); + return false; + } + return true; + }) + .collect(Collectors.toList()); + + log.info("Filter MONEY prizes completed in multi-draw. userRechargeAmount: {}, beforeCount: {}, afterCount: {}", + userRechargeAmount, prizes.size(), filtered.size()); + + return filtered; + } + /** * 标记用户已中BADGE类型奖品(永久记录). *