抽奖新增小号校验

This commit is contained in:
tianfeng 2025-12-30 21:09:14 +08:00
parent 55d827efd7
commit b21a7b46a6
7 changed files with 231 additions and 11 deletions

View File

@ -159,6 +159,11 @@ public enum OtherErrorCode implements IResponseErrorCode {
*/
USER_RECHARGE_INVALID(3273, "The recharge amount is insufficient to claim the reward"),
/**
* 设备已被其他账号使用
*/
DEVICE_ALREADY_USED(3274, "Device has been used by another account"),
;
private final int code;

View File

@ -13,6 +13,7 @@ import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicketRecord;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserCount;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserProgress;
import com.red.circle.other.infra.database.rds.entity.user.user.LatestMobileDevice;
import com.red.circle.other.infra.database.rds.service.activity.LotteryActivityService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryPrizeService;
import com.red.circle.other.infra.database.rds.service.activity.LotteryRecordService;
@ -20,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.entity.activity.LotteryDeviceRecord;
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 +64,8 @@ public class LotteryDrawExe {
private final LotteryUserProgressService lotteryUserProgressService;
private final LotteryPrizeGrantService lotteryPrizeGrantService;
private final RedisService redisService;
private final LatestMobileDeviceService latestMobileDeviceService;
private final LotteryDeviceRecordService lotteryDeviceRecordService;
@Transactional(rollbackFor = Exception.class)
public LotteryDrawResultCO execute(LotteryDrawCmd cmd) {
@ -69,19 +76,21 @@ public class LotteryDrawExe {
// 1. 校验活动
LotteryActivity activity = validateActivity(activityId, now);
// 2. 校验抽奖次数限制
validateDrawCount(userId, activityId, activity);
// 2. 校验设备唯一性防止小号薅羊毛
checkDeviceUnique(activityId, userId);
// 3. 校验并扣除抽奖券如果需要
// 3. 校验抽奖次数限制
validateDrawCount(userId, activityId, activity);
// 4. 校验并扣除抽奖券如果需要
deductTicket(userId, activity.getTicketCost());
// 4. 获取或创建用户进度用于动态概率
// 5. 获取或创建用户进度用于动态概率
LotteryUserProgress progress = getOrCreateUserProgress(userId, activityId);
// 5. 执行抽奖算法动态概率
// 6. 执行抽奖算法动态概率
LotteryPrize prize = drawPrizeWithDynamicProbability(activityId, activity, progress);
// 6. 扣减奖品库存使用悲观锁防止超卖
// 7. 扣减奖品库存使用悲观锁防止超卖
if (prize != null) {
boolean deductSuccess = deductPrizeStock(prize.getId());
if (!deductSuccess) {
@ -90,24 +99,62 @@ public class LotteryDrawExe {
}
}
// 7. 保存抽奖记录
// 8. 保存抽奖记录
LotteryRecord record = saveDrawRecord(userId, activityId, prize, now, null, 1);
// 8. 发放奖励
// 9. 发放奖励
if (prize != null) {
lotteryPrizeGrantService.grantPrize(userId, prize, record.getRecordNo());
}
// 9. 更新用户抽奖次数
// 10. 更新用户抽奖次数
updateUserDrawCount(userId, activityId);
// 10. 更新用户进度累加金额和次数
// 11. 更新用户进度累加金额和次数
updateUserProgress(progress, prize);
// 11. 组装返回结果
// 12. 记录设备信息首次抽奖时
recordDeviceAfterDraw(activityId, userId);
// 13. 组装返回结果
return convertToResultCO(record, prize);
}
/**
* 校验设备唯一性防止小号薅羊毛
*/
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, userId: {}", userId);
return;
}
LotteryDeviceRecord existingRecord = lotteryDeviceRecordService
.getByActivityAndFingerprint(activityId, fingerprint);
if (existingRecord != null && !existingRecord.getUserId().equals(userId)) {
log.warn("Duplicate device detected, 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, activityId: {}, userId: {}", activityId, userId, e);
}
}
/**
* 校验活动状态和时间.
*/

View File

@ -0,0 +1,13 @@
package com.red.circle.other.infra.database.rds.dao.activity;
import com.red.circle.framework.mybatis.dao.BaseDAO;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryDeviceRecord;
/**
* 抽奖设备记录 DAO
*
* @author system
* @since 2025-12-30
*/
public interface LotteryDeviceRecordDAO extends BaseDAO<LotteryDeviceRecord> {
}

View File

@ -0,0 +1,60 @@
package com.red.circle.other.infra.database.rds.entity.activity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serial;
import java.io.Serializable;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 抽奖设备记录表
*
* @author system
* @since 2025-12-30
*/
@Data
@Accessors(chain = true)
@TableName("lottery_device_record")
public class LotteryDeviceRecord implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键标识
*/
@TableId("id")
private Long id;
/**
* 活动ID
*/
@TableField("activity_id")
private Long activityId;
/**
* 用户ID
*/
@TableField("user_id")
private Long userId;
/**
* 设备指纹MD5
*/
@TableField("device_fingerprint")
private String deviceFingerprint;
/**
* 创建时间
*/
@TableField("create_time")
private Long createTime;
/**
* 更新时间
*/
@TableField("update_time")
private Long updateTime;
}

View File

@ -0,0 +1,31 @@
package com.red.circle.other.infra.database.rds.service.activity;
import com.red.circle.framework.mybatis.service.BaseService;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryDeviceRecord;
/**
* 抽奖设备记录 Service
*
* @author system
* @since 2025-12-30
*/
public interface LotteryDeviceRecordService extends BaseService<LotteryDeviceRecord> {
/**
* 根据活动ID和设备指纹查询记录
*
* @param activityId 活动ID
* @param fingerprint 设备指纹
* @return 设备记录
*/
LotteryDeviceRecord getByActivityAndFingerprint(Long activityId, String fingerprint);
/**
* 记录设备信息幂等操作
*
* @param activityId 活动ID
* @param userId 用户ID
* @param fingerprint 设备指纹
*/
void recordDevice(Long activityId, Long userId, String fingerprint);
}

View File

@ -0,0 +1,58 @@
package com.red.circle.other.infra.database.rds.service.activity.impl;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.other.infra.database.rds.dao.activity.LotteryDeviceRecordDAO;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryDeviceRecord;
import com.red.circle.other.infra.database.rds.service.activity.LotteryDeviceRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
/**
* 抽奖设备记录 Service实现
*
* @author system
* @since 2025-12-30
*/
@Slf4j
@Service
public class LotteryDeviceRecordServiceImpl
extends BaseServiceImpl<LotteryDeviceRecordDAO, LotteryDeviceRecord>
implements LotteryDeviceRecordService {
@Override
public LotteryDeviceRecord getByActivityAndFingerprint(Long activityId, String fingerprint) {
if (activityId == null || StringUtils.isBlank(fingerprint)) {
return null;
}
return query()
.eq(LotteryDeviceRecord::getActivityId, activityId)
.eq(LotteryDeviceRecord::getDeviceFingerprint, fingerprint)
.getOne();
}
@Override
public void recordDevice(Long activityId, Long userId, String fingerprint) {
LotteryDeviceRecord existing = query()
.eq(LotteryDeviceRecord::getActivityId, activityId)
.eq(LotteryDeviceRecord::getUserId, userId)
.getOne();
if (existing == null) {
LotteryDeviceRecord record = new LotteryDeviceRecord();
record.setActivityId(activityId);
record.setUserId(userId);
record.setDeviceFingerprint(fingerprint);
record.setCreateTime(System.currentTimeMillis());
record.setUpdateTime(System.currentTimeMillis());
try {
save(record);
} catch (DuplicateKeyException e) {
log.warn("Device record already exists, activityId:{}, userId:{}", activityId, userId);
}
}
}
}

View File

@ -32,6 +32,7 @@ import com.red.circle.other.domain.ranking.RankingCycleType;
import com.red.circle.other.domain.ranking.RankingDimension;
import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService;
import com.red.circle.other.infra.database.rds.entity.sys.GameListConfig;
import com.red.circle.other.infra.database.rds.entity.user.user.LatestMobileDevice;
import com.red.circle.other.infra.database.rds.entity.user.user.OneTimeTask;
import com.red.circle.other.infra.database.rds.service.activity.UserActivityRechargeService;
import com.red.circle.other.infra.database.rds.service.sys.GameListConfigService;
@ -99,6 +100,11 @@ public class SpringTest {
@Autowired
private SingleGroupRewardPoolCommon singleGroupRewardPoolCommon;
@Test
public void testLatestDevice() {
String latestMobileDevice = latestMobileDeviceService.getEnhancedDeviceFingerprintByUserId(1957345312961527809L);
System.out.println();
}
@Test
public void testActivityRuleConfig() {