抽奖新增小号校验
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"),
|
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;
|
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.LotteryTicketRecord;
|
||||||
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserCount;
|
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.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.LotteryActivityService;
|
||||||
import com.red.circle.other.infra.database.rds.service.activity.LotteryPrizeService;
|
import com.red.circle.other.infra.database.rds.service.activity.LotteryPrizeService;
|
||||||
import com.red.circle.other.infra.database.rds.service.activity.LotteryRecordService;
|
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.LotteryTicketService;
|
||||||
import com.red.circle.other.infra.database.rds.service.activity.LotteryUserCountService;
|
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.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.infra.utils.ZonedDateTimeUtils;
|
||||||
|
import com.red.circle.other.inner.asserts.OtherErrorCode;
|
||||||
import com.red.circle.other.inner.asserts.lottery.LotteryErrorCode;
|
import com.red.circle.other.inner.asserts.lottery.LotteryErrorCode;
|
||||||
import com.red.circle.tool.core.date.TimestampUtils;
|
import com.red.circle.tool.core.date.TimestampUtils;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@ -59,6 +64,8 @@ public class LotteryDrawExe {
|
|||||||
private final LotteryUserProgressService lotteryUserProgressService;
|
private final LotteryUserProgressService lotteryUserProgressService;
|
||||||
private final LotteryPrizeGrantService lotteryPrizeGrantService;
|
private final LotteryPrizeGrantService lotteryPrizeGrantService;
|
||||||
private final RedisService redisService;
|
private final RedisService redisService;
|
||||||
|
private final LatestMobileDeviceService latestMobileDeviceService;
|
||||||
|
private final LotteryDeviceRecordService lotteryDeviceRecordService;
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public LotteryDrawResultCO execute(LotteryDrawCmd cmd) {
|
public LotteryDrawResultCO execute(LotteryDrawCmd cmd) {
|
||||||
@ -69,19 +76,21 @@ public class LotteryDrawExe {
|
|||||||
// 1. 校验活动
|
// 1. 校验活动
|
||||||
LotteryActivity activity = validateActivity(activityId, now);
|
LotteryActivity activity = validateActivity(activityId, now);
|
||||||
|
|
||||||
// 2. 校验抽奖次数限制
|
// 2. 校验设备唯一性(防止小号薅羊毛)
|
||||||
validateDrawCount(userId, activityId, activity);
|
checkDeviceUnique(activityId, userId);
|
||||||
|
|
||||||
// 3. 校验并扣除抽奖券(如果需要)
|
// 3. 校验抽奖次数限制
|
||||||
|
validateDrawCount(userId, activityId, activity);
|
||||||
|
// 4. 校验并扣除抽奖券(如果需要)
|
||||||
deductTicket(userId, activity.getTicketCost());
|
deductTicket(userId, activity.getTicketCost());
|
||||||
|
|
||||||
// 4. 获取或创建用户进度(用于动态概率)
|
// 5. 获取或创建用户进度(用于动态概率)
|
||||||
LotteryUserProgress progress = getOrCreateUserProgress(userId, activityId);
|
LotteryUserProgress progress = getOrCreateUserProgress(userId, activityId);
|
||||||
|
|
||||||
// 5. 执行抽奖算法(动态概率)
|
// 6. 执行抽奖算法(动态概率)
|
||||||
LotteryPrize prize = drawPrizeWithDynamicProbability(activityId, activity, progress);
|
LotteryPrize prize = drawPrizeWithDynamicProbability(activityId, activity, progress);
|
||||||
|
|
||||||
// 6. 扣减奖品库存(使用悲观锁防止超卖)
|
// 7. 扣减奖品库存(使用悲观锁防止超卖)
|
||||||
if (prize != null) {
|
if (prize != null) {
|
||||||
boolean deductSuccess = deductPrizeStock(prize.getId());
|
boolean deductSuccess = deductPrizeStock(prize.getId());
|
||||||
if (!deductSuccess) {
|
if (!deductSuccess) {
|
||||||
@ -90,24 +99,62 @@ public class LotteryDrawExe {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. 保存抽奖记录
|
// 8. 保存抽奖记录
|
||||||
LotteryRecord record = saveDrawRecord(userId, activityId, prize, now, null, 1);
|
LotteryRecord record = saveDrawRecord(userId, activityId, prize, now, null, 1);
|
||||||
|
|
||||||
// 8. 发放奖励
|
// 9. 发放奖励
|
||||||
if (prize != null) {
|
if (prize != null) {
|
||||||
lotteryPrizeGrantService.grantPrize(userId, prize, record.getRecordNo());
|
lotteryPrizeGrantService.grantPrize(userId, prize, record.getRecordNo());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. 更新用户抽奖次数
|
// 10. 更新用户抽奖次数
|
||||||
updateUserDrawCount(userId, activityId);
|
updateUserDrawCount(userId, activityId);
|
||||||
|
|
||||||
// 10. 更新用户进度(累加金额和次数)
|
// 11. 更新用户进度(累加金额和次数)
|
||||||
updateUserProgress(progress, prize);
|
updateUserProgress(progress, prize);
|
||||||
|
|
||||||
// 11. 组装返回结果
|
// 12. 记录设备信息(首次抽奖时)
|
||||||
|
recordDeviceAfterDraw(activityId, userId);
|
||||||
|
|
||||||
|
// 13. 组装返回结果
|
||||||
return convertToResultCO(record, prize);
|
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.domain.ranking.RankingDimension;
|
||||||
import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService;
|
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.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.entity.user.user.OneTimeTask;
|
||||||
import com.red.circle.other.infra.database.rds.service.activity.UserActivityRechargeService;
|
import com.red.circle.other.infra.database.rds.service.activity.UserActivityRechargeService;
|
||||||
import com.red.circle.other.infra.database.rds.service.sys.GameListConfigService;
|
import com.red.circle.other.infra.database.rds.service.sys.GameListConfigService;
|
||||||
@ -99,6 +100,11 @@ public class SpringTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private SingleGroupRewardPoolCommon singleGroupRewardPoolCommon;
|
private SingleGroupRewardPoolCommon singleGroupRewardPoolCommon;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLatestDevice() {
|
||||||
|
String latestMobileDevice = latestMobileDeviceService.getEnhancedDeviceFingerprintByUserId(1957345312961527809L);
|
||||||
|
System.out.println();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testActivityRuleConfig() {
|
public void testActivityRuleConfig() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user