Java 徒手寫一個抽獎系統!拿去用吧!
點選“終碼一生”,關注,置頂公眾號
每日技術乾貨,第一時間送達!
1、概述
專案開發中經常會有抽獎這樣的營銷活動的需求,例如:積分大轉盤、刮刮樂、老虎雞等等多種形式,其實後臺的實現方法是一樣的,本文介紹一種常用的抽獎實現方法。
整個抽獎過程包括以下幾個方面:
-
獎品
-
獎品池
-
抽獎演算法
-
獎品限制
-
獎品發放
2、獎品
獎品包括獎品、獎品概率和限制、獎品記錄。
獎品表:
CREATETABLE`points_luck_draw_prize`(
`id`bigint(20)NOTNULLAUTO_INCREMENT,
`name`varchar(50)DEFAULTNULLCOMMENT'獎品名稱',
`url`varchar(50)DEFAULTNULLCOMMENT'圖片地址',
`value`varchar(20)DEFAULTNULL,
`type`tinyint(4)DEFAULTNULLCOMMENT'型別1:紅包2:積分3:體驗金4:謝謝惠顧5:自定義',
`status`tinyint(4)DEFAULTNULLCOMMENT'狀態',
`is_del`bit(1)DEFAULTNULLCOMMENT'是否刪除',
`position`int(5)DEFAULTNULLCOMMENT'位置',
`phase`int(10)DEFAULTNULLCOMMENT'期數',
`create_time`datetimeDEFAULTNULL,
`update_time`datetimeDEFAULTNULL,
PRIMARYKEY(`id`)
)ENGINE=InnoDBAUTO_INCREMENT=164DEFAULTCHARSET=utf8mb4COMMENT='獎品表';
獎品概率限制表:
CREATETABLE`points_luck_draw_probability`(
`id`bigint(20)NOTNULLAUTO_INCREMENT,
`points_prize_id`bigint(20)DEFAULTNULLCOMMENT'獎品ID',
`points_prize_phase`int(10)DEFAULTNULLCOMMENT'獎品期數',
`probability`float(4,2)DEFAULTNULLCOMMENT'概率',
`frozen`int(11)DEFAULTNULLCOMMENT'商品抽中後的冷凍次數',
`prize_day_max_times`int(11)DEFAULTNULLCOMMENT'該商品平臺每天最多抽中的次數',
`user_prize_month_max_times`int(11)DEFAULTNULLCOMMENT'每位使用者每月最多抽中該商品的次數',
`create_time`datetimeDEFAULTNULL,
`update_time`datetimeDEFAULTNULL,
PRIMARYKEY(`id`)
)ENGINE=InnoDBAUTO_INCREMENT=114DEFAULTCHARSET=utf8mb4COMMENT='抽獎概率限制表';
獎品記錄表:
CREATETABLE`points_luck_draw_record`(
`id`bigint(20)NOTNULLAUTO_INCREMENT,
`member_id`bigint(20)DEFAULTNULLCOMMENT'使用者ID',
`member_mobile`varchar(11)DEFAULTNULLCOMMENT'中獎使用者手機號',
`points`int(11)DEFAULTNULLCOMMENT'消耗積分',
`prize_id`bigint(20)DEFAULTNULLCOMMENT'獎品ID',
`result`smallint(4)DEFAULTNULLCOMMENT'1:中獎 2:未中獎',
`month`varchar(10)DEFAULTNULLCOMMENT'中獎月份',
`daily`dateDEFAULTNULLCOMMENT'中獎日期(不包括時間)',
`create_time`datetimeDEFAULTNULL,
`update_time`datetimeDEFAULTNULL,
PRIMARYKEY(`id`)
)ENGINE=InnoDBAUTO_INCREMENT=3078DEFAULTCHARSET=utf8mb4COMMENT='抽獎記錄表';
3、獎品池
獎品池是根據獎品的概率和限制組裝成的抽獎用的池子。主要包括獎品的總池值和每個獎品所佔的池值(分為開始值和結束值)兩個維度。
-
獎品的總池值:所有獎品池值的總和。
-
每個獎品的池值:演算法可以變通,常用的有以下兩種方式 :
-
獎品的概率*10000(保證是整數)
-
獎品的概率10000獎品的剩餘數量
獎品池bean:
publicclassPrizePoolimplementsSerializable{
/**
* 總池值
*/
privateint total;
/**
* 池中的獎品
*/
privateList<PrizePoolBean> poolBeanList;
}
池中的獎品bean:
publicclassPrizePoolBeanimplementsSerializable{
/**
* 資料庫中真實獎品的ID
*/
privateLong id;
/**
* 獎品的開始池值
*/
privateint begin;
/**
* 獎品的結束池值
*/
privateint end;
}
獎品池的組裝程式碼:
/**
* 獲取超級大富翁的獎品池
*@paramzillionaireProductMap 超級大富翁獎品map
*@paramflag true:有現金 false:無現金
*@return
*/
privatePrizePoolgetZillionairePrizePool(Map<Long, ActivityProduct> zillionaireProductMap,booleanflag){
//總的獎品池值
inttotal =0;
List<PrizePoolBean> poolBeanList =newArrayList<>();
for(Entry<Long, ActivityProduct> entry : zillionaireProductMap.entrySet()){
ActivityProduct product = entry.getValue();
//無現金獎品池,過濾掉型別為現金的獎品
if(!flag && product.getCategoryId() == ActivityPrizeTypeEnums.XJ.getType()){
continue;
}
//組裝獎品池獎品
PrizePoolBean prizePoolBean =newPrizePoolBean();
prizePoolBean.setId(product.getProductDescriptionId());
prizePoolBean.setBengin(total);
total = total + product.getEarnings().multiply(newBigDecimal("10000")).intValue();
prizePoolBean.setEnd(total);
poolBeanList.add(prizePoolBean);
}
PrizePool prizePool =newPrizePool();
prizePool.setTotal(total);
prizePool.setPoolBeanList(poolBeanList);
returnprizePool;
}
4、抽獎演算法
整個抽獎演算法為:
1. 隨機獎品池總池值以內的整數
2. 迴圈比較獎品池中的所有獎品,隨機數落到哪個獎品的池區間即為哪個獎品中獎。
抽獎程式碼:
publicstaticPrizePoolBeangetPrize(PrizePool prizePool){
//獲取總的獎品池值
inttotal = prizePool.getTotal();
//獲取隨機數
Random rand=newRandom();
intrandom=rand.nextInt(total);
//迴圈比較獎品池區間
for(PrizePoolBean prizePoolBean : prizePool.getPoolBeanList()){
if(random >= prizePoolBean.getBengin() && random < prizePoolBean.getEnd()){
returnprizePoolBean;
}
}
returnnull;
}
5、獎品限制
實際抽獎中對一些比較大的獎品往往有數量限制,比如:某某獎品一天最多被抽中5次、某某獎品每位使用者只能抽中一次。。等等類似的限制,對於這樣的限制我們分為兩種情況來區別對待:
1. 限制的獎品比較少,通常不多於3個:這種情況我們可以再組裝獎品池的時候就把不符合條件的獎品過濾掉,這樣抽中的獎品都是符合條件的。例如,在上面的超級大富翁抽獎程式碼中,我們規定現金獎品一天只能被抽中5次,那麼我們可以根據判斷條件分別組裝出有現金的獎品和沒有現金的獎品。
2. 限制的獎品比較多,這樣如果要採用第一種方式,就會導致組裝獎品非常繁瑣,效能低下,我們可以採用抽中獎品後校驗抽中的獎品是否符合條件,如果不符合條件則返回一個固定的獎品即可。
6、獎品發放
獎品發放可以採用工廠模式進行發放:不同的獎品型別走不同的獎品發放處理器,示例程式碼如下:
獎品發放:
/**
* 非同步分發獎品
*@paramprizeList
*@throwsException
*/
@Async("myAsync")
@Transactional(rollbackFor =Exception.class, propagation = Propagation.REQUIRED)
publicFuture<Boolean> sendPrize(Long memberId,List<PrizeDto> prizeList){
try{
for(PrizeDto prizeDto : prizeList){
//過濾掉謝謝惠顧的獎品
if(prizeDto.getType() == PointsLuckDrawTypeEnum.XXHG.getType()){
continue;
}
//根據獎品型別從工廠中獲取獎品發放類
SendPrizeProcessor sendPrizeProcessor = sendPrizeProcessorFactory.getSendPrizeProcessor(
PointsLuckDrawTypeEnum.getPointsLuckDrawTypeEnumByType(prizeDto.getType()));
if(ObjectUtil.isNotNull(sendPrizeProcessor)){
//發放獎品
sendPrizeProcessor.send(memberId, prizeDto);
}
}
returnnewAsyncResult<>(Boolean.TRUE);
}catch(Exceptione){
//獎品發放失敗則記錄日誌
saveSendPrizeErrorLog(memberId, prizeList);
LOGGER.error("積分抽獎發放獎品出現異常", e);
returnnewAsyncResult<>(Boolean.FALSE);
}
}
工廠類:
@Component
publicclassSendPrizeProcessorFactoryimplementsApplicationContextAware{
privateApplicationContext applicationContext;
@Override
publicvoidsetApplicationContext(ApplicationContext applicationContext)throwsBeansException{
this.applicationContext = applicationContext;
}
publicSendPrizeProcessorgetSendPrizeProcessor(PointsLuckDrawTypeEnum typeEnum){
String processorName = typeEnum.getSendPrizeProcessorName();
if(StrUtil.isBlank(processorName)){
returnnull;
}
SendPrizeProcessor processor = applicationContext.getBean(processorName, SendPrizeProcessor.class);
if(ObjectUtil.isNull(processor)){
thrownewRuntimeException("沒有找到名稱為【"+ processorName +"】的傳送獎品處理器");
}
returnprocessor;
}
}
獎品發放類舉例:
/**
* 紅包獎品發放類
*/
@Component("sendHbPrizeProcessor")
publicclassSendHbPrizeProcessorimplementsSendPrizeProcessor{
privateLogger LOGGER = LoggerFactory.getLogger(SendHbPrizeProcessor.class);
@Resource
privateCouponService couponService;
@Resource
privateMessageLogService messageLogService;
@Override
publicvoidsend(Long memberId, PrizeDto prizeDto)throwsException{
// 發放紅包
Coupon coupon = couponService.receiveCoupon(memberId, Long.parseLong(prizeDto.getValue()));
//傳送站內信
messageLogService.insertActivityMessageLog(memberId,
"你參與積分抽大獎活動抽中的"+ coupon.getAmount() +"元理財紅包已到賬,謝謝參與",
"積分抽大獎中獎通知");
//輸出log日誌
LOGGER.info(memberId +"在積分抽獎中抽中的"+ prizeDto.getPrizeName() +"已經發放!");
}
}
PS:防止找不到本篇文章,可以收藏點贊,方便翻閱查詢哦。