抽奖新增小号校验
This commit is contained in:
parent
55d827efd7
commit
b21a7b46a6
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验活动状态和时间.
|
||||
*/
|
||||
|
||||
@ -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> {
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user