抽奖新增多层概率池
This commit is contained in:
parent
5b42ea8840
commit
5b43823751
@ -10,24 +10,29 @@ import com.red.circle.other.infra.database.rds.entity.activity.LotteryRecord;
|
|||||||
import com.red.circle.other.infra.database.rds.entity.activity.LotteryTicket;
|
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.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;
|
||||||
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketRecordService;
|
import com.red.circle.other.infra.database.rds.service.activity.LotteryTicketRecordService;
|
||||||
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.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;
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.temporal.WeekFields;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@ -48,6 +53,7 @@ public class LotteryDrawExe {
|
|||||||
private final LotteryTicketService lotteryTicketService;
|
private final LotteryTicketService lotteryTicketService;
|
||||||
private final LotteryTicketRecordService lotteryTicketRecordService;
|
private final LotteryTicketRecordService lotteryTicketRecordService;
|
||||||
private final LotteryUserCountService lotteryUserCountService;
|
private final LotteryUserCountService lotteryUserCountService;
|
||||||
|
private final LotteryUserProgressService lotteryUserProgressService;
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public LotteryDrawResultCO execute(LotteryDrawCmd cmd) {
|
public LotteryDrawResultCO execute(LotteryDrawCmd cmd) {
|
||||||
@ -61,30 +67,36 @@ public class LotteryDrawExe {
|
|||||||
// 2. 校验抽奖次数限制
|
// 2. 校验抽奖次数限制
|
||||||
validateDrawCount(userId, activityId, activity);
|
validateDrawCount(userId, activityId, activity);
|
||||||
|
|
||||||
// 3. 校验并扣除抽奖券(写死消耗1次)
|
// 3. 校验并扣除抽奖券(如果需要)
|
||||||
Integer needTicket = 1;
|
if (activity.getNeedTicket() == 1) {
|
||||||
deductTicket(userId, needTicket);
|
deductTicket(userId, activity.getTicketCost());
|
||||||
|
}
|
||||||
|
|
||||||
// 4. 执行抽奖算法
|
// 4. 获取或创建用户进度(用于动态概率)
|
||||||
LotteryPrize prize = drawPrize(activityId);
|
LotteryUserProgress progress = getOrCreateUserProgress(userId, activityId);
|
||||||
|
|
||||||
// 5. 扣减奖品库存(使用悲观锁防止超卖)
|
// 5. 执行抽奖算法(动态概率)
|
||||||
|
LotteryPrize prize = drawPrizeWithDynamicProbability(activityId, activity, progress);
|
||||||
|
|
||||||
|
// 6. 扣减奖品库存(使用悲观锁防止超卖)
|
||||||
if (prize != null) {
|
if (prize != null) {
|
||||||
boolean deductSuccess = deductPrizeStock(prize.getId());
|
boolean deductSuccess = deductPrizeStock(prize.getId());
|
||||||
if (!deductSuccess) {
|
if (!deductSuccess) {
|
||||||
// 库存扣减失败,视为未中奖
|
|
||||||
log.warn("Prize stock deduction failed, prizeId: {}", prize.getId());
|
log.warn("Prize stock deduction failed, prizeId: {}", prize.getId());
|
||||||
prize = null;
|
prize = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 保存抽奖记录
|
// 7. 保存抽奖记录
|
||||||
LotteryRecord record = saveDrawRecord(userId, activityId, prize, now);
|
LotteryRecord record = saveDrawRecord(userId, activityId, prize, now, null, 1);
|
||||||
|
|
||||||
// 7. 更新用户抽奖次数
|
// 8. 更新用户抽奖次数
|
||||||
updateUserDrawCount(userId, activityId);
|
updateUserDrawCount(userId, activityId);
|
||||||
|
|
||||||
// 8. 组装返回结果
|
// 9. 更新用户进度(累加金额和次数)
|
||||||
|
updateUserProgress(progress, prize);
|
||||||
|
|
||||||
|
// 10. 组装返回结果
|
||||||
return convertToResultCO(record, prize);
|
return convertToResultCO(record, prize);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,67 +133,69 @@ public class LotteryDrawExe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扣除抽奖券(优化:使用批量处理,减少数据库交互).
|
* 获取或创建用户进度.
|
||||||
*/
|
*/
|
||||||
private void deductTicket(Long userId, Integer ticketCost) {
|
private LotteryUserProgress getOrCreateUserProgress(Long userId, Long activityId) {
|
||||||
// 查询可用抽奖券(按过期时间排序,先用快过期的)
|
String weekKey = getWeekKey();
|
||||||
List<LotteryTicket> tickets = lotteryTicketService.query()
|
|
||||||
.eq(LotteryTicket::getUserId, userId)
|
|
||||||
.eq(LotteryTicket::getStatus, 1)
|
|
||||||
.gt(LotteryTicket::getRemainingCount, 0)
|
|
||||||
.orderByAsc(LotteryTicket::getExpireTime)
|
|
||||||
.list();
|
|
||||||
|
|
||||||
// 校验总数量
|
|
||||||
int totalRemaining = tickets.stream()
|
|
||||||
.mapToInt(LotteryTicket::getRemainingCount)
|
|
||||||
.sum();
|
|
||||||
ResponseAssert.isTrue(LotteryErrorCode.INSUFFICIENT_TICKETS, totalRemaining >= ticketCost);
|
|
||||||
|
|
||||||
// 批量扣减
|
|
||||||
List<LotteryTicketRecord> records = new ArrayList<>();
|
|
||||||
int remaining = ticketCost;
|
|
||||||
|
|
||||||
for (LotteryTicket ticket : tickets) {
|
LotteryUserProgress progress = lotteryUserProgressService.query()
|
||||||
if (remaining <= 0) {
|
.eq(LotteryUserProgress::getUserId, userId)
|
||||||
break;
|
.eq(LotteryUserProgress::getActivityId, activityId)
|
||||||
}
|
.eq(LotteryUserProgress::getWeekKey, weekKey)
|
||||||
|
.getOne();
|
||||||
|
|
||||||
int deduct = Math.min(remaining, ticket.getRemainingCount());
|
if (progress == null) {
|
||||||
|
progress = new LotteryUserProgress();
|
||||||
// 使用乐观锁更新(防止并发问题)
|
progress.setUserId(userId);
|
||||||
boolean updateSuccess = lotteryTicketService.update()
|
progress.setActivityId(activityId);
|
||||||
.eq(LotteryTicket::getId, ticket.getId())
|
progress.setWeekKey(weekKey);
|
||||||
.eq(LotteryTicket::getRemainingCount, ticket.getRemainingCount()) // 乐观锁
|
progress.setTotalAmount(BigDecimal.ZERO);
|
||||||
.setSql("used_count = used_count + " + deduct)
|
progress.setTotalDrawCount(0);
|
||||||
.setSql("remaining_count = remaining_count - " + deduct)
|
progress.setBigPrizeCount(0);
|
||||||
.execute();
|
progress.setCurrentStage("EARLY");
|
||||||
|
progress.setLastDrawDate(LocalDate.now());
|
||||||
ResponseAssert.isTrue(LotteryErrorCode.INSUFFICIENT_TICKETS, updateSuccess);
|
lotteryUserProgressService.save(progress);
|
||||||
|
|
||||||
// 记录使用日志
|
|
||||||
LotteryTicketRecord ticketRecord = new LotteryTicketRecord();
|
|
||||||
ticketRecord.setUserId(userId);
|
|
||||||
ticketRecord.setTicketId(ticket.getId());
|
|
||||||
ticketRecord.setChangeCount(-deduct);
|
|
||||||
ticketRecord.setChangeType(2); // 消耗
|
|
||||||
ticketRecord.setRemark("抽奖消耗");
|
|
||||||
records.add(ticketRecord);
|
|
||||||
|
|
||||||
remaining -= deduct;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量插入日志
|
return progress;
|
||||||
if (!records.isEmpty()) {
|
|
||||||
lotteryTicketRecordService.saveBatch(records);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 抽奖算法 - 权重随机(优化:使用ThreadLocalRandom).
|
* 动态概率抽奖.
|
||||||
*/
|
*/
|
||||||
private LotteryPrize drawPrize(Long activityId) {
|
private LotteryPrize drawPrizeWithDynamicProbability(Long activityId, LotteryActivity activity, LotteryUserProgress progress) {
|
||||||
// 查询所有可用奖品
|
// 判断是否启用动态概率
|
||||||
|
if (activity.getEnableDynamicProbability() == null || activity.getEnableDynamicProbability() != 1) {
|
||||||
|
// 未启用动态概率,使用普通抽奖
|
||||||
|
return drawPrizeNormal(activityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 判断用户所处阶段
|
||||||
|
String stage = determinePrizePool(progress, activity);
|
||||||
|
log.info("User stage: {}, userId: {}, drawCount: {}, totalAmount: {}",
|
||||||
|
stage, progress.getUserId(), progress.getTotalDrawCount(), progress.getTotalAmount());
|
||||||
|
|
||||||
|
// 2. 查询对应奖品池的奖品
|
||||||
|
List<LotteryPrize> prizes = getPrizesByPool(activityId, stage);
|
||||||
|
|
||||||
|
// 3. 过滤会超过提现门槛的大奖
|
||||||
|
prizes = filterOverThresholdPrizes(prizes, progress, activity.getWithdrawThreshold());
|
||||||
|
|
||||||
|
if (prizes.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 动态调整概率
|
||||||
|
adjustProbability(prizes, progress, activity.getWithdrawThreshold());
|
||||||
|
|
||||||
|
// 5. 执行抽奖
|
||||||
|
return randomSelectByProbability(prizes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 普通抽奖(不使用动态概率).
|
||||||
|
*/
|
||||||
|
private LotteryPrize drawPrizeNormal(Long activityId) {
|
||||||
List<LotteryPrize> prizes = lotteryPrizeService.query()
|
List<LotteryPrize> prizes = lotteryPrizeService.query()
|
||||||
.eq(LotteryPrize::getActivityId, activityId)
|
.eq(LotteryPrize::getActivityId, activityId)
|
||||||
.gt(LotteryPrize::getRemainingStock, 0)
|
.gt(LotteryPrize::getRemainingStock, 0)
|
||||||
@ -192,11 +206,132 @@ public class LotteryDrawExe {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return randomSelectByProbability(prizes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断奖品池阶段.
|
||||||
|
*/
|
||||||
|
private String determinePrizePool(LotteryUserProgress progress, LotteryActivity activity) {
|
||||||
|
int drawCount = progress.getTotalDrawCount();
|
||||||
|
BigDecimal totalAmount = progress.getTotalAmount();
|
||||||
|
BigDecimal threshold = activity.getWithdrawThreshold() != null
|
||||||
|
? activity.getWithdrawThreshold()
|
||||||
|
: new BigDecimal("10");
|
||||||
|
|
||||||
|
// 默认阶段配置
|
||||||
|
int earlyMax = activity.getEarlyStageMaxCount() != null ? activity.getEarlyStageMaxCount() : 20;
|
||||||
|
int middleMax = activity.getMiddleStageMaxCount() != null ? activity.getMiddleStageMaxCount() : 50;
|
||||||
|
BigDecimal lateTrigger = activity.getLateStageTriggerAmount() != null
|
||||||
|
? activity.getLateStageTriggerAmount()
|
||||||
|
: new BigDecimal("2");
|
||||||
|
|
||||||
|
// 后期池:抽奖次数多 且 接近门槛
|
||||||
|
BigDecimal gap = threshold.subtract(totalAmount);
|
||||||
|
if (drawCount >= middleMax && gap.compareTo(lateTrigger) <= 0) {
|
||||||
|
return "LATE";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 中期池:抽奖次数中等
|
||||||
|
if (drawCount >= earlyMax) {
|
||||||
|
return "MIDDLE";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前期池
|
||||||
|
return "EARLY";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据奖品池查询奖品.
|
||||||
|
*/
|
||||||
|
private List<LotteryPrize> getPrizesByPool(Long activityId, String pool) {
|
||||||
|
return lotteryPrizeService.query()
|
||||||
|
.eq(LotteryPrize::getActivityId, activityId)
|
||||||
|
.gt(LotteryPrize::getRemainingStock, 0)
|
||||||
|
.and(wrapper -> wrapper
|
||||||
|
.eq(LotteryPrize::getPrizePool, pool)
|
||||||
|
.or()
|
||||||
|
.eq(LotteryPrize::getPrizePool, "ALL")
|
||||||
|
)
|
||||||
|
.orderByAsc(LotteryPrize::getSortOrder)
|
||||||
|
.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 过滤会超过门槛的奖品.
|
||||||
|
*/
|
||||||
|
private List<LotteryPrize> filterOverThresholdPrizes(List<LotteryPrize> prizes,
|
||||||
|
LotteryUserProgress progress,
|
||||||
|
BigDecimal threshold) {
|
||||||
|
if (threshold == null) {
|
||||||
|
return prizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal currentAmount = progress.getTotalAmount();
|
||||||
|
|
||||||
|
// 如果已经达到门槛,本周不再发放任何奖品
|
||||||
|
if (currentAmount.compareTo(threshold) >= 0) {
|
||||||
|
log.info("User reached threshold, no more prizes this week. userId: {}, amount: {}",
|
||||||
|
progress.getUserId(), currentAmount);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return prizes.stream()
|
||||||
|
.filter(prize -> {
|
||||||
|
// 过滤掉会让用户超过门槛的大奖
|
||||||
|
BigDecimal afterAmount = currentAmount.add(prize.getPrizeValue());
|
||||||
|
return afterAmount.compareTo(threshold) <= 0;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态调整概率.
|
||||||
|
*/
|
||||||
|
private void adjustProbability(List<LotteryPrize> prizes, LotteryUserProgress progress, BigDecimal threshold) {
|
||||||
|
if (threshold == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal totalAmount = progress.getTotalAmount();
|
||||||
|
BigDecimal gap = threshold.subtract(totalAmount); // 距离门槛还差多少
|
||||||
|
|
||||||
|
for (LotteryPrize prize : prizes) {
|
||||||
|
BigDecimal prizeValue = prize.getPrizeValue();
|
||||||
|
BigDecimal originalProbability = prize.getProbability();
|
||||||
|
|
||||||
|
// 如果这个奖品能让用户达到或接近门槛,提升概率
|
||||||
|
BigDecimal diff = gap.subtract(prizeValue).abs();
|
||||||
|
if (diff.compareTo(new BigDecimal("0.5")) <= 0) {
|
||||||
|
// 差距在0.5美元内,概率翻倍
|
||||||
|
prize.setProbability(originalProbability.multiply(new BigDecimal("2")));
|
||||||
|
log.debug("Boost prize probability x2, prizeName: {}, gap: {}", prize.getPrizeName(), gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户已经很接近门槛(>9美元),大幅提升能凑整的小奖概率
|
||||||
|
if (totalAmount.compareTo(new BigDecimal("9")) > 0) {
|
||||||
|
if (prizeValue.compareTo(gap) <= 0 && prizeValue.compareTo(new BigDecimal("0.5")) >= 0) {
|
||||||
|
prize.setProbability(originalProbability.multiply(new BigDecimal("3")));
|
||||||
|
log.debug("Boost prize probability x3 (near threshold), prizeName: {}", prize.getPrizeName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按概率随机选择奖品.
|
||||||
|
*/
|
||||||
|
private LotteryPrize randomSelectByProbability(List<LotteryPrize> prizes) {
|
||||||
// 计算总概率
|
// 计算总概率
|
||||||
BigDecimal totalProbability = prizes.stream()
|
BigDecimal totalProbability = prizes.stream()
|
||||||
.map(LotteryPrize::getProbability)
|
.map(LotteryPrize::getProbability)
|
||||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
if (totalProbability.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机数
|
||||||
double randomValue = ThreadLocalRandom.current().nextDouble() * totalProbability.doubleValue();
|
double randomValue = ThreadLocalRandom.current().nextDouble() * totalProbability.doubleValue();
|
||||||
double cumulative = 0.0;
|
double cumulative = 0.0;
|
||||||
|
|
||||||
@ -212,37 +347,87 @@ public class LotteryDrawExe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扣减奖品库存(使用悲观锁防止超卖).
|
* 扣除抽奖券.
|
||||||
|
*/
|
||||||
|
private void deductTicket(Long userId, Integer ticketCost) {
|
||||||
|
List<LotteryTicket> tickets = lotteryTicketService.query()
|
||||||
|
.eq(LotteryTicket::getUserId, userId)
|
||||||
|
.eq(LotteryTicket::getStatus, 1)
|
||||||
|
.gt(LotteryTicket::getRemainingCount, 0)
|
||||||
|
.orderByAsc(LotteryTicket::getExpireTime)
|
||||||
|
.list();
|
||||||
|
|
||||||
|
int totalRemaining = tickets.stream()
|
||||||
|
.mapToInt(LotteryTicket::getRemainingCount)
|
||||||
|
.sum();
|
||||||
|
ResponseAssert.isTrue(LotteryErrorCode.INSUFFICIENT_TICKETS, totalRemaining >= ticketCost);
|
||||||
|
|
||||||
|
List<LotteryTicketRecord> records = new ArrayList<>();
|
||||||
|
int remaining = ticketCost;
|
||||||
|
|
||||||
|
for (LotteryTicket ticket : tickets) {
|
||||||
|
if (remaining <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int deduct = Math.min(remaining, ticket.getRemainingCount());
|
||||||
|
|
||||||
|
boolean updateSuccess = lotteryTicketService.update()
|
||||||
|
.eq(LotteryTicket::getId, ticket.getId())
|
||||||
|
.eq(LotteryTicket::getRemainingCount, ticket.getRemainingCount())
|
||||||
|
.setSql("used_count = used_count + " + deduct)
|
||||||
|
.setSql("remaining_count = remaining_count - " + deduct)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
ResponseAssert.isTrue(LotteryErrorCode.INSUFFICIENT_TICKETS, updateSuccess);
|
||||||
|
|
||||||
|
LotteryTicketRecord ticketRecord = new LotteryTicketRecord();
|
||||||
|
ticketRecord.setUserId(userId);
|
||||||
|
ticketRecord.setTicketId(ticket.getId());
|
||||||
|
ticketRecord.setChangeCount(-deduct);
|
||||||
|
ticketRecord.setChangeType(2);
|
||||||
|
ticketRecord.setRemark("抽奖消耗");
|
||||||
|
records.add(ticketRecord);
|
||||||
|
|
||||||
|
remaining -= deduct;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!records.isEmpty()) {
|
||||||
|
lotteryTicketRecordService.saveBatch(records);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 扣减奖品库存.
|
||||||
*/
|
*/
|
||||||
private boolean deductPrizeStock(Long prizeId) {
|
private boolean deductPrizeStock(Long prizeId) {
|
||||||
// 使用数据库级别的原子操作
|
return lotteryPrizeService.update()
|
||||||
int affected = lotteryPrizeService.update()
|
|
||||||
.eq(LotteryPrize::getId, prizeId)
|
.eq(LotteryPrize::getId, prizeId)
|
||||||
.gt(LotteryPrize::getRemainingStock, 0) // 必须有库存
|
.gt(LotteryPrize::getRemainingStock, 0)
|
||||||
.setSql("remaining_stock = remaining_stock - 1")
|
.setSql("remaining_stock = remaining_stock - 1")
|
||||||
.execute()
|
.execute();
|
||||||
? 1 : 0;
|
|
||||||
|
|
||||||
return affected > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存抽奖记录.
|
* 保存抽奖记录.
|
||||||
*/
|
*/
|
||||||
private LotteryRecord saveDrawRecord(Long userId, Long activityId, LotteryPrize prize, Timestamp now) {
|
private LotteryRecord saveDrawRecord(Long userId, Long activityId, LotteryPrize prize,
|
||||||
|
Timestamp now, String batchNo, Integer drawType) {
|
||||||
String recordNo = generateRecordNo();
|
String recordNo = generateRecordNo();
|
||||||
LotteryRecord record = new LotteryRecord();
|
LotteryRecord record = new LotteryRecord();
|
||||||
record.setRecordNo(recordNo);
|
record.setRecordNo(recordNo);
|
||||||
record.setUserId(userId);
|
record.setUserId(userId);
|
||||||
record.setActivityId(activityId);
|
record.setActivityId(activityId);
|
||||||
record.setDrawTime(now);
|
record.setDrawTime(now);
|
||||||
|
record.setDrawBatchNo(batchNo);
|
||||||
|
record.setDrawType(drawType);
|
||||||
|
|
||||||
if (prize != null) {
|
if (prize != null) {
|
||||||
record.setPrizeId(prize.getId());
|
record.setPrizeId(prize.getId());
|
||||||
record.setPrizeName(prize.getPrizeName());
|
record.setPrizeName(prize.getPrizeName());
|
||||||
record.setPrizeType(prize.getPrizeType());
|
record.setPrizeType(prize.getPrizeType());
|
||||||
record.setIsWin(1);
|
record.setIsWin(1);
|
||||||
record.setPrizeStatus(0); // 待发放
|
record.setPrizeStatus(0);
|
||||||
} else {
|
} else {
|
||||||
record.setIsWin(0);
|
record.setIsWin(0);
|
||||||
}
|
}
|
||||||
@ -252,12 +437,11 @@ public class LotteryDrawExe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新用户抽奖次数(优化:减少查询).
|
* 更新用户抽奖次数.
|
||||||
*/
|
*/
|
||||||
private void updateUserDrawCount(Long userId, Long activityId) {
|
private void updateUserDrawCount(Long userId, Long activityId) {
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
|
|
||||||
// 先尝试直接更新
|
|
||||||
boolean updated = lotteryUserCountService.update()
|
boolean updated = lotteryUserCountService.update()
|
||||||
.eq(LotteryUserCount::getUserId, userId)
|
.eq(LotteryUserCount::getUserId, userId)
|
||||||
.eq(LotteryUserCount::getActivityId, activityId)
|
.eq(LotteryUserCount::getActivityId, activityId)
|
||||||
@ -265,7 +449,6 @@ public class LotteryDrawExe {
|
|||||||
.setSql("draw_count = draw_count + 1")
|
.setSql("draw_count = draw_count + 1")
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// 如果更新失败(记录不存在),则插入
|
|
||||||
if (!updated) {
|
if (!updated) {
|
||||||
try {
|
try {
|
||||||
LotteryUserCount userCount = new LotteryUserCount();
|
LotteryUserCount userCount = new LotteryUserCount();
|
||||||
@ -275,7 +458,6 @@ public class LotteryDrawExe {
|
|||||||
userCount.setDrawCount(1);
|
userCount.setDrawCount(1);
|
||||||
lotteryUserCountService.save(userCount);
|
lotteryUserCountService.save(userCount);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 并发情况下可能插入失败,再次更新
|
|
||||||
lotteryUserCountService.update()
|
lotteryUserCountService.update()
|
||||||
.eq(LotteryUserCount::getUserId, userId)
|
.eq(LotteryUserCount::getUserId, userId)
|
||||||
.eq(LotteryUserCount::getActivityId, activityId)
|
.eq(LotteryUserCount::getActivityId, activityId)
|
||||||
@ -287,7 +469,33 @@ public class LotteryDrawExe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成记录编号(优化:使用更高效的方式).
|
* 更新用户进度.
|
||||||
|
*/
|
||||||
|
private void updateUserProgress(LotteryUserProgress progress, LotteryPrize prize) {
|
||||||
|
BigDecimal addAmount = prize != null ? prize.getPrizeValue() : BigDecimal.ZERO;
|
||||||
|
int addBigPrizeCount = (prize != null && prize.getPrizeValue().compareTo(new BigDecimal("0.5")) >= 0) ? 1 : 0;
|
||||||
|
|
||||||
|
lotteryUserProgressService.update()
|
||||||
|
.eq(LotteryUserProgress::getId, progress.getId())
|
||||||
|
.setSql("total_amount = total_amount + " + addAmount)
|
||||||
|
.setSql("total_draw_count = total_draw_count + 1")
|
||||||
|
.setSql("big_prize_count = big_prize_count + " + addBigPrizeCount)
|
||||||
|
.set(LotteryUserProgress::getLastDrawDate, LocalDate.now())
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成周标识.
|
||||||
|
*/
|
||||||
|
private String getWeekKey() {
|
||||||
|
LocalDate now = LocalDate.now();
|
||||||
|
int year = now.getYear();
|
||||||
|
int weekOfYear = now.get(WeekFields.of(Locale.getDefault()).weekOfYear());
|
||||||
|
return String.format("%dW%02d", year, weekOfYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成记录编号.
|
||||||
*/
|
*/
|
||||||
private String generateRecordNo() {
|
private String generateRecordNo() {
|
||||||
return "LT" + System.currentTimeMillis() +
|
return "LT" + System.currentTimeMillis() +
|
||||||
@ -305,13 +513,7 @@ public class LotteryDrawExe {
|
|||||||
|
|
||||||
if (prize != null) {
|
if (prize != null) {
|
||||||
LotteryPrizeCO prizeCO = new LotteryPrizeCO();
|
LotteryPrizeCO prizeCO = new LotteryPrizeCO();
|
||||||
prizeCO.setId(prize.getId());
|
BeanUtils.copyProperties(prize, prizeCO);
|
||||||
prizeCO.setPrizeCode(prize.getPrizeCode());
|
|
||||||
prizeCO.setPrizeName(prize.getPrizeName());
|
|
||||||
prizeCO.setPrizeType(prize.getPrizeType());
|
|
||||||
prizeCO.setPrizeValue(prize.getPrizeValue());
|
|
||||||
prizeCO.setPrizeImage(prize.getPrizeImage());
|
|
||||||
prizeCO.setPrizeLevel(prize.getPrizeLevel());
|
|
||||||
result.setPrize(prizeCO);
|
result.setPrize(prizeCO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
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.LotteryUserProgress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户抽奖进度Mapper.
|
||||||
|
*
|
||||||
|
* @author system
|
||||||
|
* @since 2025-10-20
|
||||||
|
*/
|
||||||
|
public interface LotteryUserProgressMapper extends BaseDAO<LotteryUserProgress> {
|
||||||
|
|
||||||
|
}
|
||||||
@ -144,4 +144,34 @@ public class LotteryActivity implements Serializable {
|
|||||||
@TableField("multi_draw_guarantee_min_amount")
|
@TableField("multi_draw_guarantee_min_amount")
|
||||||
private BigDecimal multiDrawGuaranteeMinAmount;
|
private BigDecimal multiDrawGuaranteeMinAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用动态概率:0-否,1-是.
|
||||||
|
*/
|
||||||
|
@TableField("enable_dynamic_probability")
|
||||||
|
private Integer enableDynamicProbability;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提现门槛金额.
|
||||||
|
*/
|
||||||
|
@TableField("withdraw_threshold")
|
||||||
|
private BigDecimal withdrawThreshold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 前期池抽奖次数上限.
|
||||||
|
*/
|
||||||
|
@TableField("early_stage_max_count")
|
||||||
|
private Integer earlyStageMaxCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 中期池抽奖次数上限.
|
||||||
|
*/
|
||||||
|
@TableField("middle_stage_max_count")
|
||||||
|
private Integer middleStageMaxCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后期池触发金额(距离门槛).
|
||||||
|
*/
|
||||||
|
@TableField("late_stage_trigger_amount")
|
||||||
|
private BigDecimal lateStageTriggerAmount;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,6 +111,18 @@ public class LotteryPrize implements Serializable {
|
|||||||
@TableField("is_guarantee_prize")
|
@TableField("is_guarantee_prize")
|
||||||
private Integer isGuaranteePrize;
|
private Integer isGuaranteePrize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 奖品池:EARLY-前期池,MIDDLE-中期池,LATE-后期池,ALL-全部.
|
||||||
|
*/
|
||||||
|
@TableField("prize_pool")
|
||||||
|
private String prizePool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每周发放上限(0不限制).
|
||||||
|
*/
|
||||||
|
@TableField("week_limit")
|
||||||
|
private Integer weekLimit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 排序(九宫格位置).
|
* 排序(九宫格位置).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -0,0 +1,89 @@
|
|||||||
|
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 com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 用户抽奖进度表.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author system
|
||||||
|
* @since 2025-10-20
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
|
@TableName("lottery_user_progress")
|
||||||
|
public class LotteryUserProgress {
|
||||||
|
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键标识.
|
||||||
|
*/
|
||||||
|
@TableId("id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID.
|
||||||
|
*/
|
||||||
|
@TableField("user_id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动ID.
|
||||||
|
*/
|
||||||
|
@TableField("activity_id")
|
||||||
|
private Long activityId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 周标识:2025W42.
|
||||||
|
*/
|
||||||
|
@TableField("week_key")
|
||||||
|
private String weekKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本周累计金额.
|
||||||
|
*/
|
||||||
|
@TableField("total_amount")
|
||||||
|
private BigDecimal totalAmount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本周抽奖次数.
|
||||||
|
*/
|
||||||
|
@TableField("total_draw_count")
|
||||||
|
private Integer totalDrawCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本周大奖次数.
|
||||||
|
*/
|
||||||
|
@TableField("big_prize_count")
|
||||||
|
private Integer bigPrizeCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前所处阶段:EARLY-前期,MIDDLE-中期,LATE-后期.
|
||||||
|
*/
|
||||||
|
@TableField("current_stage")
|
||||||
|
private String currentStage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上次抽奖日期.
|
||||||
|
*/
|
||||||
|
@TableField("last_draw_date")
|
||||||
|
private LocalDate lastDrawDate;
|
||||||
|
|
||||||
|
private Timestamp createTime;
|
||||||
|
|
||||||
|
private Timestamp updateTime;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
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.LotteryUserProgress;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户抽奖进度服务.
|
||||||
|
*
|
||||||
|
* @author system
|
||||||
|
* @since 2025-10-20
|
||||||
|
*/
|
||||||
|
public interface LotteryUserProgressService extends BaseService<LotteryUserProgress> {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.red.circle.other.infra.database.rds.service.activity.impl;
|
||||||
|
|
||||||
|
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
|
||||||
|
import com.red.circle.other.infra.database.rds.entity.activity.LotteryUserProgress;
|
||||||
|
import com.red.circle.other.infra.database.rds.dao.activity.LotteryUserProgressMapper;
|
||||||
|
import com.red.circle.other.infra.database.rds.service.activity.LotteryUserProgressService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户抽奖进度服务实现.
|
||||||
|
*
|
||||||
|
* @author system
|
||||||
|
* @since 2025-10-20
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class LotteryUserProgressServiceImpl extends BaseServiceImpl<LotteryUserProgressMapper, LotteryUserProgress>
|
||||||
|
implements LotteryUserProgressService {
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper
|
||||||
|
namespace="com.red.circle.other.infra.database.rds.dao.activity.LotteryUserProgressMapper">
|
||||||
|
|
||||||
|
</mapper>
|
||||||
Loading…
x
Reference in New Issue
Block a user