基本思路:用戶生成一個隨機數,和出獎的獎品設置的隨機數比對一下。符合規則則中獎(用戶的隨機數< 獎品設置的概率值),不符則未中獎。
一 項目準備期,需求確認。
和產品大哥一陣切磋後,認爲需求1.0
//1 抽獎活動有起止時間
//2 獎品有限制個數的大獎,和不限次數的小獎。爲了要用戶開心,每抽必中。至於成本什麼的,把抽獎回報率設好,按標準線來。
//3 後來有位產品大哥說,可否做個代碼,讓內定的人中指定的獎品。
//(代碼並不難,但基於開發的底線,我斷然拒絕了,當一個企業這種文化佔主導位置時,直接離開是比較好的選擇)
//4 抽獎前端是用老虎機,還是砸蛋,還是轉盤。 中選擇了老虎機模式。
//5 每個用戶簽到後,贈送三次免費機會,剩餘的使用積分進行抽獎。 抽獎時,優先使用免費抽獎機會,然後再使用積分抽獎。單用戶每天限抽10次。
//6 產品大哥一翻成本估算,市場調研,自我分析後,定下來獎品分12 個等級。前8級有數量限制 最後四級無數量限制。
//獎品類型(實物大獎,投資紅包,會員積分,抽獎機會)
//7 用戶可以看見他的抽獎紀錄。
//8 指定獎品,要在指定時間後纔可以出獎。(避免第一天大獎被中完)
得到需要後,工作開始。 //備註,爲簡化描述,後面部分代碼及表格進行簡化。
2 表格建立。
(1 用戶表 2 活動表 3 獎品表 4 中獎紀錄表 )
1 用戶表。demo_user2 抽獎活動表 demo_lottery
3 獎品表。demo_prizes
用repeat_type 來區分是否可重複中獎。 prize_status 記錄是否中過獎。
start_time 設置最早出獎時間
prize_type 來區分獎勵什麼東西
prize_amount 記錄獎多少。
(真實項目中,例如獎紅包時,有使用紅包限制,例如滿多少纔可以用紅包,紅包使用範圍等,還有這個獎品限在哪個抽獎活動中等 這裏簡化處理。暫略)
4 中獎紀錄表。 demo_prize_log
3 一點點數據準備工作。
//準備了三個用戶,創建了一個抽獎活動,設置了四個獎勵,兩個實物(單次) 兩個虛擬獎(可多次中)
//初始化用戶數據。
$sql = "truncate demo_user";
Yii::app()->db->createCommand($sql)->execute();
$sql = "INSERT INTO demo_user SET id = 1,username = 'user1',free_chance = 1 , points = 100";
$bool = Yii::app()->db->createCommand($sql)->execute();if( !$bool ){ throw new Exception("執行失敗".$sql); }
$sql = "INSERT INTO demo_user SET id = 2,username = 'user2',free_chance = 2 , points = 800";
$bool = Yii::app()->db->createCommand($sql)->execute();if( !$bool ){ throw new Exception("執行失敗".$sql); }
$sql = "INSERT INTO demo_user SET id = 3,username = 'user3',free_chance = 3 , points =999";
$bool = Yii::app()->db->createCommand($sql)->execute();if( !$bool ){ throw new Exception("執行失敗".$sql); }
//初始化活動數據。
$sql = "truncate demo_lottery";
Yii::app()->db->createCommand($sql)->execute();
$start_time = time();
$end_time = $start_time + 86400; //一天後期
$sql = "insert into demo_lottery set id = 1, title = '抽獎活動1' ,status =1,start_time = $start_time, end_time = $end_time , spend_point = 10 ";
echo $sql;
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
$sql = "truncate demo_prizes"; //清空獎品池
Yii::app()->db->createCommand($sql)->execute();
//一等獎,中獎概率爲10 % ,僅限中一次。
$sql = "insert into demo_prizes set `level` = 1, title = 'ihpone',start_time = $start_time , rand_num = 10,repeat_type = 'once',prize_type = 'real_goods'";
$bool = Yii::app()->db->createCommand($sql)->execute(); if( !$bool ){ throw new Exception("執行失敗".$sql); }
//二等獎,中獎概率爲10 % ,僅限中一次。
$sql = "insert into demo_prizes set `level` = 2, title = '掃地機器人',start_time = $start_time , rand_num = 10,repeat_type = 'once',prize_type = 'real_goods'";
$bool = Yii::app()->db->createCommand($sql)->execute(); if( !$bool ){ throw new Exception("執行失敗".$sql); }
//三等等獎,中獎概率爲20 % ,不限中獎次數。
$sql = "insert into demo_prizes set `level` = 3, title = '888積分',start_time = $start_time , rand_num = 20,repeat_type = 'repeatable',prize_type = 'points',prize_amount = 888";
$bool = Yii::app()->db->createCommand($sql)->execute(); if( !$bool ){ throw new Exception("執行失敗".$sql); }
//四等等等獎,中獎概率爲80 % ,不限中獎次數。
$sql = "insert into demo_prizes set `level` = 4, title = '3次抽獎機會',start_time = $start_time , rand_num = 80,repeat_type = 'repeatable',prize_type = 'lottery_chance',prize_amount=3";
$bool = Yii::app()->db->createCommand($sql)->execute(); if( !$bool ){ throw new Exception("執行失敗".$sql); }
$sql = "truncate demo_prize_log"; //清空中獎記錄。
Yii::app()->db->createCommand($sql)->execute();
4 第一個版本的抽獎代碼。
public function lottery($lottery_id , $uid){
if(!preg_match("/^\d+$/" , $lottery_id.$uid)) { exit("參數異常");} //小習慣,碰到數值型參數,驗一道
//確認活動是否開啓。
$sql = "select * from demo_lottery where id = $lottery_id ";
$lotteryRow = Yii::app()->db->createCommand($sql)->queryRow();
if( !$lotteryRow ) exit( "活動不存在!" );
$time = time();
if( $time < $lotteryRow["start_time"] || $time > $lotteryRow["end_time"]) { exit("活動暫未開始或已結束");}
$sql = "select * from demo_user where id = $uid";
$userRow = Yii::app()->db->createCommand($sql)->queryRow();
if( $userRow["free_chance"] + $userRow["points"]/$lotteryRow["spend_point"] < 1) { exit("無抽獎機會");}
try {
$trans = Yii::app()->db->beginTransaction();
//取出獎品數據
$sql = "select * from demo_prizes where start_time < $time && prize_status = 1 order by level asc";
$prizesRows = Yii::app()->db->createCommand($sql)->queryAll();
$user_rand = rand(1,100); //生成一個用戶隨機數.
$lotteryPrize = array(); //用戶抽中的獎品
$temp = 0;
foreach( $prizesRows as $key => $prizeRow ): //一個個的比對。
$temp = $temp + $prizeRow["rand_num"];
if( $temp > $user_rand ) {
$lotteryPrize = $prizeRow; //抽中
break;
}
endforeach;
//生成客戶中獎紀錄,並獎勵用戶。
$msg = "恭喜抽中".$lotteryPrize["level"]."等獎".$lotteryPrize["title"];
$lottery_type = $userRow["free_chance"]>0?"free_chance":"points"; //用戶使用哪種方式抽獎。
$sql = "insert into demo_prize_log set uid = $uid ,prize_id = ".$lotteryPrize["id"].",create_at =$time,msg='$msg',lottery_type = '$lottery_type'";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
//扣除掉用戶的抽獎機會.
if( $lottery_type == "free_chance"){
$sql = "update demo_user set free_chance = free_chance - 1 where id = $uid";
$bool = Yii::app()->db->createCommand($sql)->execute(); if( !$bool ){ throw new Exception("執行失敗".$sql); }
}else{
$spend_point = $lotteryRow["spend_point"];
$sql = "update demo_user set points = points - $spend_point where id = $uid";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
}
//最後,執行獎勵部分。 根據不同的類型進行發獎。
$prize_amount = $lotteryPrize["prize_amount"];
switch ($lotteryPrize["prize_type"])
{
case "points":
//執行積分獎勵。
$sql = "update demo_user set points = points +$prize_amount where id = $uid";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
break;
case "lottery_chance":
//執行抽獎獎勵。
$sql = "update demo_user set free_chance = free_chance +$prize_amount where id = $uid";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
break;
default:
break;
}
//更新單次獎品的狀態。變爲不可抽獎.
if( $lotteryPrize["repeat_type"] == "once"){
$sql = "update demo_prizes set prize_status = 2 where id = ".$lotteryPrize["id"];
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
}
$trans->commit();
return true;
} catch (Exception $e) {
Yii::log($e->getMessage(), CLogger::LEVEL_INFO, "log_error");
$trans->rollback();
return false;
}
測試哥一測,給出反饋如下。
//單次抽獎是沒問題,一併發就出現了問題。比如將一等獎概率變大,一個一等獎被有可能被中兩次。
於是折騰了下代碼,使用mysql 行鎖處理。可以查看另一篇blog--> php 使用msyql 行鎖防止高併發請求時扣庫存異常 【防爆單,超賣】
調整後代碼如下:
public function lottery($lottery_id , $uid){
if(!preg_match("/^\d+$/" , $lottery_id.$uid)) { exit("參數異常");} //小習慣,碰到數值型參數,驗一道
//確認活動是否開啓。
$sql = "select * from demo_lottery where id = $lottery_id ";
$lotteryRow = Yii::app()->db->createCommand($sql)->queryRow();
if( !$lotteryRow ) exit( "活動不存在!" );
$time = time();
if( $time < $lotteryRow["start_time"] || $time > $lotteryRow["end_time"]) { exit("活動暫未開始或已結束");}
try {
$trans = Yii::app()->db->beginTransaction();
$sql = "select * from demo_user where id = $uid for update"; //對用戶數據加行鎖
$userRow = Yii::app()->db->createCommand($sql)->queryRow();
if( $userRow["free_chance"] + $userRow["points"]/$lotteryRow["spend_point"] < 1) { exit("無抽獎機會");}
//取出獎品數據
$sql = "select * from demo_prizes where start_time < $time && prize_status = 1 order by level asc for update"; //對獎品數據加行鎖
$prizesRows = Yii::app()->db->createCommand($sql)->queryAll();
$user_rand = rand(1,100); //生成一個用戶隨機數.
$lotteryPrize = array(); //用戶抽中的獎品
$temp = 0;
foreach( $prizesRows as $key => $prizeRow ): //一個個的比對。
$temp = $temp + $prizeRow["rand_num"];
if( $temp > $user_rand ) {
$lotteryPrize = $prizeRow; //抽中
break;
}
endforeach;
//生成客戶中獎紀錄,並獎勵用戶。
$msg = "恭喜抽中".$lotteryPrize["level"]."等獎".$lotteryPrize["title"];
$lottery_type = $userRow["free_chance"]>0?"free_chance":"points"; //用戶使用哪種方式抽獎。
$sql = "insert into demo_prize_log set uid = $uid ,prize_id = ".$lotteryPrize["id"].",create_at =$time,msg='$msg',lottery_type = '$lottery_type'";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
//扣除掉用戶的抽獎機會.
if( $lottery_type == "free_chance"){
$sql = "update demo_user set free_chance = free_chance - 1 where id = $uid";
$bool = Yii::app()->db->createCommand($sql)->execute(); if( !$bool ){ throw new Exception("執行失敗".$sql); }
}else{
$spend_point = $lotteryRow["spend_point"];
$sql = "update demo_user set points = points - $spend_point where id = $uid";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
}
//最後,執行獎勵部分。 根據不同的類型進行發獎。
$prize_amount = $lotteryPrize["prize_amount"];
switch ($lotteryPrize["prize_type"])
{
case "points":
//執行積分獎勵。
$sql = "update demo_user set points = points +$prize_amount where id = $uid";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
break;
case "lottery_chance":
//執行抽獎獎勵。
$sql = "update demo_user set free_chance = free_chance +$prize_amount where id = $uid";
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
break;
default:
break;
}
//更新單次獎品的狀態。變爲不可抽獎.
if( $lotteryPrize["repeat_type"] == "once"){
$sql = "update demo_prizes set prize_status = 2 where id = ".$lotteryPrize["id"];
$bool = Yii::app()->db->createCommand($sql)->execute();
if( !$bool ){ throw new Exception("執行失敗".$sql); }
}
$trans->commit();
return true;
} catch (Exception $e) {
Yii::log($e->getMessage(), CLogger::LEVEL_INFO, "log_error");
$trans->rollback();
return false;
}
}
一個基礎版的抽獎就出來了。下一篇講如何對當前代碼進行有效的重構。使其應對產品大哥的各種需求的變化。
更值得一看的是下一篇 --》 php 實現抽獎代碼詳解【中篇】 面對需求變更