基本思路:用户生成一个随机数,和出奖的奖品设置的随机数比对一下。符合规则则中奖(用户的随机数< 奖品设置的概率值),不符则未中奖。
一 项目准备期,需求确认。
和产品大哥一阵切磋后,认为需求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 实现抽奖代码详解【中篇】 面对需求变更