@ -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.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.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 ;
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.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.tool.core.date.TimestampUtils ;
import java.math.BigDecimal ;
import java.sql.Timestamp ;
import java.time.LocalDate ;
import java.time.format.DateTimeFormatter ;
import java.time.temporal.WeekFields ;
import java.util.ArrayList ;
import java.util.List ;
import java.util.Random ;
import java.util.UUID ;
import java.util.Locale ;
import java.util.concurrent.ThreadLocalRandom ;
import java.util.stream.Collectors ;
import lombok.RequiredArgsConstructor ;
import lombok.extern.slf4j.Slf4j ;
import org.springframework.beans.BeanUtils ;
import org.springframework.stereotype.Component ;
import org.springframework.transaction.annotation.Transactional ;
@ -48,6 +53,7 @@ public class LotteryDrawExe {
private final LotteryTicketService lotteryTicketService ;
private final LotteryTicketRecordService lotteryTicketRecordService ;
private final LotteryUserCountService lotteryUserCountService ;
private final LotteryUserProgressService lotteryUserProgressService ;
@Transactional ( rollbackFor = Exception . class )
public LotteryDrawResultCO execute ( LotteryDrawCmd cmd ) {
@ -61,30 +67,36 @@ public class LotteryDrawExe {
/ / 2 . 校验抽奖次数限制
validateDrawCount ( userId , activityId , activity ) ;
/ / 3 . 校验并扣除抽奖券 ( 写死消耗1次 )
Integer needTicket = 1 ;
deductTicket ( userId , needTicket ) ;
/ / 3 . 校验并扣除抽奖券 ( 如果需要 )
if ( activity . getNeedTicket ( ) = = 1 ) {
deductTicket ( userId , activity . getTicketCost ( ) ) ;
}
/ / 4 . 执行抽奖算法
Lottery Prize prize = drawPrize ( activityId ) ;
/ / 4 . 获取或创建用户进度( 用于动态概率 )
Lottery UserProgress progress = getOrCreateUserProgress ( userId , activityId ) ;
/ / 5 . 扣减奖品库存 ( 使用悲观锁防止超卖 )
/ / 5 . 执行抽奖算法 ( 动态概率 )
LotteryPrize prize = drawPrizeWithDynamicProbability ( activityId , activity , progress ) ;
/ / 6 . 扣减奖品库存 ( 使用悲观锁防止超卖 )
if ( prize ! = null ) {
boolean deductSuccess = deductPrizeStock ( prize . getId ( ) ) ;
if ( ! deductSuccess ) {
/ / 库存扣减失败 , 视为未中奖
log . warn ( " Prize stock deduction failed, prizeId: {} " , prize . getId ( ) ) ;
prize = null ;
}
}
/ / 6 . 保存抽奖记录
LotteryRecord record = saveDrawRecord ( userId , activityId , prize , now );
/ / 7 . 保存抽奖记录
LotteryRecord record = saveDrawRecord ( userId , activityId , prize , now , null , 1 );
/ / 7 . 更新用户抽奖次数
/ / 8 . 更新用户抽奖次数
updateUserDrawCount ( userId , activityId ) ;
/ / 8 . 组装返回结果
/ / 9 . 更新用户进度 ( 累加金额和次数 )
updateUserProgress ( progress , prize ) ;
/ / 10 . 组装返回结果
return convertToResultCO ( record , prize ) ;
}
@ -121,67 +133,69 @@ 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 ;
private LotteryUserProgress getOrCreateUserProgress ( Long userId , Long activityId ) {
String weekKey = getWeekKey ( ) ;
for ( LotteryTicket ticket : tickets ) {
if ( remaining < = 0 ) {
break ;
}
LotteryUserProgress progress = lotteryUserProgressService . query ( )
. eq ( LotteryUserProgress : : getUserId , userId )
. eq ( LotteryUserProgress : : getActivityId , activityId )
. eq ( LotteryUserProgress : : getWeekKey , weekKey )
. getOne ( ) ;
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 ( progress = = null ) {
progress = new LotteryUserProgress ( ) ;
progress . setUserId ( userId ) ;
progress . setActivityId ( activityId ) ;
progress . setWeekKey ( weekKey ) ;
progress . setTotalAmount ( BigDecimal . ZERO ) ;
progress . setTotalDrawCount ( 0 ) ;
progress . setBigPrizeCount ( 0 ) ;
progress . setCurrentStage ( " EARLY " ) ;
progress . setLastDrawDate ( LocalDate . now ( ) ) ;
lotteryUserProgressService . save ( progress ) ;
}
/ / 批量插入日志
if ( ! records . isEmpty ( ) ) {
lotteryTicketRecordService . saveBatch ( records ) ;
}
return progress ;
}
/ * *
* 抽奖算法 - 权重随机 ( 优化 : 使用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 ( )
. eq ( LotteryPrize : : getActivityId , activityId )
. gt ( LotteryPrize : : getRemainingStock , 0 )
@ -192,11 +206,132 @@ public class LotteryDrawExe {
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 ( )
. map ( LotteryPrize : : getProbability )
. reduce ( BigDecimal . ZERO , BigDecimal : : add ) ;
if ( totalProbability . compareTo ( BigDecimal . ZERO ) = = 0 ) {
return null ;
}
/ / 生成随机数
double randomValue = ThreadLocalRandom . current ( ) . nextDouble ( ) * totalProbability . doubleValue ( ) ;
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 ) {
/ / 使用数据库级别的原子操作
int affected = lotteryPrizeService . update ( )
return lotteryPrizeService . update ( )
. eq ( LotteryPrize : : getId , prizeId )
. gt ( LotteryPrize : : getRemainingStock , 0 ) / / 必须有库存
. gt ( LotteryPrize : : getRemainingStock , 0 )
. setSql ( " remaining_stock = remaining_stock - 1 " )
. execute ( )
? 1 : 0 ;
return affected > 0 ;
. execute ( ) ;
}
/ * *
* 保存抽奖记录 .
* /
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 ( ) ;
LotteryRecord record = new LotteryRecord ( ) ;
record . setRecordNo ( recordNo ) ;
record . setUserId ( userId ) ;
record . setActivityId ( activityId ) ;
record . setDrawTime ( now ) ;
record . setDrawBatchNo ( batchNo ) ;
record . setDrawType ( drawType ) ;
if ( prize ! = null ) {
record . setPrizeId ( prize . getId ( ) ) ;
record . setPrizeName ( prize . getPrizeName ( ) ) ;
record . setPrizeType ( prize . getPrizeType ( ) ) ;
record . setIsWin ( 1 ) ;
record . setPrizeStatus ( 0 ) ; / / 待发放
record . setPrizeStatus ( 0 ) ;
} else {
record . setIsWin ( 0 ) ;
}
@ -252,12 +437,11 @@ public class LotteryDrawExe {
}
/ * *
* 更新用户抽奖次数 ( 优化 : 减少查询 ) .
* 更新用户抽奖次数 .
* /
private void updateUserDrawCount ( Long userId , Long activityId ) {
LocalDate today = LocalDate . now ( ) ;
/ / 先尝试直接更新
boolean updated = lotteryUserCountService . update ( )
. eq ( LotteryUserCount : : getUserId , userId )
. eq ( LotteryUserCount : : getActivityId , activityId )
@ -265,7 +449,6 @@ public class LotteryDrawExe {
. setSql ( " draw_count = draw_count + 1 " )
. execute ( ) ;
/ / 如果更新失败 ( 记录不存在 ) , 则插入
if ( ! updated ) {
try {
LotteryUserCount userCount = new LotteryUserCount ( ) ;
@ -275,7 +458,6 @@ public class LotteryDrawExe {
userCount . setDrawCount ( 1 ) ;
lotteryUserCountService . save ( userCount ) ;
} catch ( Exception e ) {
/ / 并发情况下可能插入失败 , 再次更新
lotteryUserCountService . update ( )
. eq ( LotteryUserCount : : getUserId , userId )
. 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 ( ) {
return " LT " + System . currentTimeMillis ( ) +
@ -305,13 +513,7 @@ public class LotteryDrawExe {
if ( prize ! = null ) {
LotteryPrizeCO prizeCO = new LotteryPrizeCO ( ) ;
prizeCO . setId ( prize . getId ( ) ) ;
prizeCO . setPrizeCode ( prize . getPrizeCode ( ) ) ;
prizeCO . setPrizeName ( prize . getPrizeName ( ) ) ;
prizeCO . setPrizeType ( prize . getPrizeType ( ) ) ;
prizeCO . setPrizeValue ( prize . getPrizeValue ( ) ) ;
prizeCO . setPrizeImage ( prize . getPrizeImage ( ) ) ;
prizeCO . setPrizeLevel ( prize . getPrizeLevel ( ) ) ;
BeanUtils . copyProperties ( prize , prizeCO ) ;
result . setPrize ( prizeCO ) ;
}