From f9f0d0194816fd2653a7d1289fb9f60190f1e7c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E5=8D=8E=E6=B8=85?= <18691022700@163.com> Date: Sun, 21 Dec 2025 16:04:34 +0800 Subject: [PATCH] =?UTF-8?q?=E7=88=86=E5=B8=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/api/controller/Lottery.php | 43 +++ application/api/controller/Room.php | 8 +- application/api/model/Chat.php | 4 + application/api/model/Lottery.php | 149 +++++++++ application/common/library/LotteryGiftLua.php | 135 ++++++++ application/common/service/LotteryService.php | 312 ++++++++++++++++++ 6 files changed, 649 insertions(+), 2 deletions(-) create mode 100644 application/api/controller/Lottery.php create mode 100644 application/api/model/Lottery.php create mode 100644 application/common/library/LotteryGiftLua.php create mode 100644 application/common/service/LotteryService.php diff --git a/application/api/controller/Lottery.php b/application/api/controller/Lottery.php new file mode 100644 index 00000000..cb40a765 --- /dev/null +++ b/application/api/controller/Lottery.php @@ -0,0 +1,43 @@ + $this->request->param('uid/d', 0), + 'prize_type' => $this->request->param('prize_type/d', 0), + 'start_time' => $this->request->param('start_time/d', 0), + 'end_time' => $this->request->param('end_time/d', time()) + ]; + $service = new LotteryService(); + $stat = $service->statWinner($where); + return json([ + 'code' => 0, + 'msg' => '统计成功', + 'data' => $stat + ]); + } catch (Exception $e) { + Log::error('中奖统计失败:' . $e->getMessage()); + return json([ + 'code' => 1, + 'msg' => $e->getMessage(), + 'data' => [] + ]); + } + } +} \ No newline at end of file diff --git a/application/api/controller/Room.php b/application/api/controller/Room.php index d0b6ecc2..d1e15539 100644 --- a/application/api/controller/Room.php +++ b/application/api/controller/Room.php @@ -183,13 +183,17 @@ class Room extends BaseCom redis_lock_exit($key_name); $room_id = input('room_id', 0); $gift_id = input('gift_id', 0); - $gift_num = input('gift_num', 0); + $gift_num = input('gift_num', 1); $to_uid = input('to_uid', 0);//收礼人ID,逗号隔开的字符串 $type = input('type', 1);//1金币购买 2送背包礼物 $pit_number = input('pit_number', 0); $heart_id = input('heart_id', 0); - $reslut = model('Room')->room_gift($this->uid, $to_uid, $gift_id, $gift_num, $type, $room_id, $pit_number,$heart_id); + if($gift_id == 88){ + $reslut = model('Lottery')->gift($this->uid, $to_uid, $gift_id, $room_id,$gift_num); + }else{ + $reslut = model('Room')->room_gift($this->uid, $to_uid, $gift_id, $gift_num, $type, $room_id, $pit_number,$heart_id); + } redis_unlock($key_name); return V($reslut['code'], $reslut['msg'], $reslut['data']); } diff --git a/application/api/model/Chat.php b/application/api/model/Chat.php index 35496411..a702d87d 100644 --- a/application/api/model/Chat.php +++ b/application/api/model/Chat.php @@ -148,6 +148,10 @@ class Chat extends Model //签约房 邀请用户上签约麦位 // SignRoomInviteUser = 1094, + //爆币房推送信息 + // BlindCoinRoom = 1100, + + diff --git a/application/api/model/Lottery.php b/application/api/model/Lottery.php new file mode 100644 index 00000000..84dcd9e9 --- /dev/null +++ b/application/api/model/Lottery.php @@ -0,0 +1,149 @@ + 0, 'msg' => '打赏礼物数量必须为整数', 'data' => null]; + } + $toarray = explode(',',$recv_uid); + if(in_array($send_uid,$toarray)){ + return ['code' => 0, 'msg' => '收礼人不能包含自己', 'data' => null]; + } + $gift_info = Db::name('vs_gift')->where(['gid'=>$gift_id]) + ->field('gid as gift_id,gift_name,gift_price,file_type,base_image,play_image,gift_type,label,is_public_server')->find(); + //送给所有人的总价格 + $all_gift_price = $gift_info['gift_price'] * $num * count($toarray); + //判断是否有足够的金币 + $user_waller = db::name('user_wallet')->where(['user_id'=>$send_uid])->find(); + if ($user_waller['coin'] < $all_gift_price) { + return ['code' => 0, 'msg' => '用户金币不足', 'data' => null]; + } + + $nums = $num * count($toarray); + $this->lottery($send_uid,$gift_info['gift_price'],$nums,$room_id,$gift_id); + + //送礼 开启事务 + Db::startTrans(); + //扣除用户金币并记录日志 + $wallet_update = model('GiveGift')->change_user_cion_or_earnings_log($send_uid,$all_gift_price,$room_id,1,10,'用户金币购买礼物'); + if(!$wallet_update){ + Db::rollback(); + return ['code' => 0, 'msg' => '扣除用户金币失败', 'data' => null]; + } + //用户财富等级更新 + $user_level = model('Level')->user_level_data_update($send_uid,$all_gift_price,1,$room_id); + if(!$user_level){ + Db::rollback(); + return ['code' => 0, 'msg' => '用户等级更新失败', 'data' => null]; + } + + //获取送礼用户昵称 + $FromUserInfo = db::name('user')->where('id',$send_uid)->field('id as user_id,nickname,avatar,sex')->find(); + $FromUserInfo['icon'][0] = model('UserData')->user_wealth_icon($send_uid);//财富图标 + $FromUserInfo['icon'][1] = model('UserData')->user_charm_icon($send_uid);//魅力图标 + $FromUserInfo['chat_bubble'] = model('Decorate')->user_decorate_detail($send_uid,9);//聊天气泡 + + //送给一人礼物的总价格(扣除用户的数额) + $gift_price = $gift_info['gift_price'] * $num; + + foreach ($toarray as $k => $to_id){ + // 1. 记录礼物赠送 + $giftRecord = [ + 'send_uid' => $send_uid, + 'recv_uid' => $recv_uid, + 'gift_id' => $gift_id, + 'gift_gold' => $gift_price, + 'recv_gold' => $gift_price /2 , + 'small_pool_add' => $gift_price /2 , + 'create_time' => time() + ]; + $giftId = Db::name('bb_lottery_gift_record')->insertGetId($giftRecord); + + //收礼记录行为日志 + $give_gift = model('GiveGift')->change_user_give_gift_log($send_uid,$gift_id,$gift_price,$num,$to_id,2,1,$room_id,0); + if(!$give_gift){ + Db::rollback(); + return ['code' => 0, 'msg' => '送礼失败', 'data' => null]; + } + //计算收礼人得益 + $receiver_earnings = model('GiveGift')->receiver_earnings($to_id,$gift_price,2); + //增加收益并记录日志 + $receiver = $this -> change_user_cion_or_earnings_log($to_id,$receiver_earnings,$room_id,2,11,'收礼增加收益'); + + //用户魅力等级更新 + $user_level = model('Level')->user_level_data_update($to_id,$gift_price,2,$room_id); + if(!$user_level){ + Db::rollback(); + return ['code' => 0, 'msg' => '用户等级更新失败', 'data' => null]; + } + + $ToUserInfo = Db::name('user')->where(['id' => $to_id])->field('id as user_id,nickname,avatar,sex')->find(); + $ToUserInfo['icon'][0] = model('UserData')->user_wealth_icon($to_id);//财富图标 + $ToUserInfo['icon'][1] = model('UserData')->user_charm_icon($to_id);//魅力图标 + $ToUserInfo['charm'] = db::name('vs_room_user_charm')->where(['user_id' => $to_id,'room_id' => $room_id])->value('charm');//魅力 + $text = $FromUserInfo['nickname'] . ' 送给 ' . $ToUserInfo['nickname'].' 礼物 ' .$gift_info['gift_name'].' x ' .$num; + $text = [ + 'FromUserInfo' => $FromUserInfo, + 'ToUserInfo' => $ToUserInfo, + 'GiftInfo' => $gift_info, + 'gift_num' => $num, + 'text' => $text + ]; + //聊天室推送系统消息 + model('Chat')->sendMsg(1005,$room_id,$text); + } + Db::commit(); + return ['code' => 1, 'msg' => '送礼成功', 'data' => null]; + } + + + //抽奖 + public function lottery($send_uid,$gift_price,$num,$room_id,$giftId) + { + try { + for($i=0;$i<$num;$i++){ + $gift_gold = $gift_price; + $service = new LotteryService(); + $reslut = $service->handleGift($send_uid, $gift_gold, $giftId); + if ($reslut['code'] == 1) { + $result = $reslut['data']; + //(未开奖时) + if ($result['is_small_prize'] == 0) { + //不做处理 + } else {//开奖 + // 大奖 + if ($result['is_big_prize'] == 1) { + $tet['text'] = '爆币大奖'; + $tet['type'] = 1; + } else { // 小奖 + $tet['text'] = '爆币小奖'; + $tet['type'] = 2; + } + $tet['user_id'] = $send_uid; + $tet['play_image'] = ''; + model('api/Chat')->sendMsg(1100,$room_id,$tet); + } + } + } + return V(1, '送礼成功'); + } catch (Exception $e) { + Log::error('抽奖处理失败:' . $e->getMessage()); + return V(0, $e->getMessage()); + } + } + +} \ No newline at end of file diff --git a/application/common/library/LotteryGiftLua.php b/application/common/library/LotteryGiftLua.php new file mode 100644 index 00000000..fa30035b --- /dev/null +++ b/application/common/library/LotteryGiftLua.php @@ -0,0 +1,135 @@ += small_trigger_times then + result.is_small_prize = 1 + -- 小奖随机比例 + local small_ratio = math.random(2, 99) + result.small_prize_amount = math.floor(small_total_gold * small_ratio / 100 * 100) / 100 + -- 小奖剩余金额(划入大奖池当前轮次) + result.small_remain_amount = math.floor((small_total_gold - result.small_prize_amount) * 100) / 100 + + -- 重置小奖池,小轮次+1 + redis.call('set', small_total_times_key, 0) + redis.call('set', small_total_gold_key, 0) + small_round = small_round + 1 + redis.call('set', small_round_key, small_round) + result.small_round = small_round + + -- 6. 小奖剩余划入大奖池当前轮次 + big_total_gold = math.floor((big_total_gold + result.small_remain_amount) * 100) / 100 + redis.call('set', big_total_gold_key, big_total_gold) + result.big_total_gold = big_total_gold + + -- 7. 大奖池开奖判断(大轮次+1) + if big_total_gold >= big_threshold then + result.is_big_prize = 1 + -- 大奖比例权重 + local weight_sum = 20 + 50 + 30 + local random_weight = math.random(1, weight_sum) + local big_ratio = random_weight <= 20 and 60 or (random_weight <= 70 and 70 or 80) + -- 大奖金额 + result.big_prize_amount = math.floor(big_total_gold * big_ratio / 100 * 100) / 100 + result.big_release_amount = math.floor((big_total_gold - result.big_prize_amount) * 100) / 100 + + -- 原有逻辑:重置大奖池,大轮次+1 + redis.call('set', big_total_gold_key, 0) + big_round = big_round + 1 + redis.call('set', big_round_key, big_round) + -- 强制保证小轮次≥大轮次 + if small_round < big_round then + small_round = big_round + redis.call('set', small_round_key, small_round) + result.small_round = small_round + end + result.big_round = big_round + result.big_total_gold = 0 + + -- ===================== 新增核心逻辑 ===================== + -- 小奖开奖金额累加到大奖池下一轮次(新的big_round) + result.small_prize_to_big_next_round = result.small_prize_amount + -- 原子性更新大奖池下一轮次金额 + local new_big_total_gold = math.floor(result.small_prize_amount * 100) / 100 + redis.call('set', big_total_gold_key, new_big_total_gold) + result.big_total_gold = new_big_total_gold + -- ====================================================== + end +end + +-- 返回结果 +return cjson.encode(result) +LUA; + + } + +} \ No newline at end of file diff --git a/application/common/service/LotteryService.php b/application/common/service/LotteryService.php new file mode 100644 index 00000000..5643708f --- /dev/null +++ b/application/common/service/LotteryService.php @@ -0,0 +1,312 @@ +redis = Cache::store('redis')->handler(); + // 加载配置 + $this->config = Db::name('bb_lottery_config')->column('value', 'key'); + // 初始化Redis缓存(若Redis数据丢失,从数据库恢复) + $this->initRedisFromDb(); + } + + + /** + * 缓存恢复:独立恢复大小轮次+对应金额 + */ + private function initRedisFromDb() + { + // 1. 恢复小奖池轮次(取pool_type=1的最大times) + $maxSmallRound = Db::name('lottery_pool_flow')->where('pool_type', 1)->max('times') ?: 1; + if (!$this->redis->get('lottery:small_pool:round')) { + $this->redis->set('lottery:small_pool:round', $maxSmallRound); + } + + // 2. 恢复大奖池轮次(取pool_type=2的最大times) + $maxBigRound = Db::name('lottery_pool_flow')->where('pool_type', 2)->max('times') ?: 1; + if (!$this->redis->get('lottery:big_pool:round')) { + $this->redis->set('lottery:big_pool:round', $maxBigRound); + } + + // 3. 恢复小奖池当前轮次的次数/金额 + $small_round = intval($this->redis->get('lottery:small_pool:round')); + if (!$this->redis->get('lottery:small_pool:total_times')) { + $smallTotalTimes = Db::name('lottery_pool_flow') + ->where(['pool_type' => 1, 'type' => 1, 'times' => $small_round]) + ->count(); + $this->redis->set('lottery:small_pool:total_times', $smallTotalTimes); + } + if (!$this->redis->get('lottery:small_pool:total_gold')) { + $smallTotalGold = Db::name('lottery_pool_flow') + ->where(['pool_type' => 1, 'type' => 1, 'times' => $small_round]) + ->sum('amount') ?: 0; + $this->redis->set('lottery:small_pool:total_gold', $smallTotalGold); + } + + // 4. 恢复大奖池当前轮次的金额 + $big_round = intval($this->redis->get('lottery:big_pool:round')); + if (!$this->redis->get('lottery:big_pool:total_gold')) { + $bigAddGold = Db::name('lottery_pool_flow') + ->where(['pool_type' => 2, 'type' => 3, 'times' => $big_round]) + ->sum('amount') ?: 0; + $bigReduceGold = Db::name('lottery_pool_flow') + ->where(['pool_type' => 2, 'type' => [2,4], 'times' => $big_round]) + ->sum('amount') ?: 0; + $this->redis->set('lottery:big_pool:total_gold', $bigAddGold + $bigReduceGold); + } + } + + + /** + * 处理送礼抽奖逻辑 + * @param int $send_uid 送礼用户ID + * @param float $gift_gold 礼物金币数 + * @param int $giftId 礼物ID + * @return array 处理结果 + * @throws Exception + */ + public function handleGift($send_uid, $gift_gold, $giftId) + { + // 参数校验 + if ($gift_gold <= 0 || !$send_uid) { + throw new Exception('参数错误'); + } + + // 读取配置+独立轮次+大奖池金额 + $small_trigger_times = intval($this->config['small_pool_trigger_times'] ?? 200); + $big_threshold = floatval($this->config['big_pool_threshold'] ?? 1000); + $small_round = intval($this->redis->get('lottery:small_pool:round') ?: 1); + $big_round = intval($this->redis->get('lottery:big_pool:round') ?: 1); + $big_total_gold = floatval($this->redis->get('lottery:big_pool:total_gold') ?: 0); + + // 加载Lua脚本 + $luaSha = LotteryGiftLua::getLotteryLuaScript(); + + // 执行Lua脚本(入参:small_round + big_round) + $result = $this->redis->evalSha($luaSha, [ + $send_uid, 0, $gift_gold, + $small_trigger_times, $big_threshold, + $small_round, $big_round, $big_total_gold + ], 0); + $result = json_decode($result, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception('Lua脚本执行失败'); + } + + // 开启数据库事务 + Db::startTrans(); + try { + // . 1记录小奖池累计流水(未开奖时) + if ($result['is_small_prize'] == 0) { + $this->addPoolFlow( + 1, // 小奖池 + 1, // 累计 + $result['small_pool_add'], + $result['small_total_gold'] - $result['small_pool_add'], + $result['small_total_gold'], + $giftId, + $result['small_round'], // 新增:传入轮次 + "小奖池累计:用户{$send_uid}送礼,轮次{$result['current_round']}" + ); + } else { + $winnerUid = $send_uid; // 奖默认给当前送礼用户 + // 1. 只要开小奖,小奖剩余划入大奖池流水 + $this->addPoolFlow( + 2, // 大奖池 + 3, // 划转 + $result['small_remain_amount'],//小奖剩余金额 + $result['big_total_gold'] - $result['small_remain_amount'], + $result['big_total_gold'], + $giftId, + $result['big_round'] - ($result['is_big_prize'] ? 1 : 0), + "小奖剩余划转大奖池:{$result['small_remain_amount']}金币" + ); + + + //2.开小奖剩余划入大奖后 大奖够开奖 + if ($result['is_big_prize'] == 1) { + // 大奖中奖记录 + $this->addWinnerRecord( + $winnerUid, + 2, // 大奖 + $result['big_prize_amount'],//中奖金额 + $result['big_total_gold'], // 开奖时大奖池金额 + $this->getBigRatio($result['big_prize_amount'], $result['big_total_gold']), + $result['big_release_amount']//释放金额 + ); + + // 大奖释放流水 + $this->addPoolFlow( + 2, // 大奖池 + 4, // 释放 + -$result['big_release_amount'],//释放金额 + $result['big_total_gold'],// 开奖时大奖池金额 + 0, + $giftId, + $result['big_round'] - 1, // 关联已结束的小奖池轮次 + "大奖释放金额:{$result['big_release_amount']}金币" + ); + + // 4. 小奖开奖金额划转下一次大奖池流水 + if ($result['small_prize_to_big_next_round'] > 0) { + $this->addPoolFlow( + 2,// 大奖池 + 3,// 划转 + $result['small_prize_to_big_next_round'], + 0, + $result['small_prize_to_big_next_round'], + $giftId, + $result['big_round'], + "小奖开奖金额划入大奖池下一轮:大轮次{$result['big_round']},金额{$result['small_prize_to_big_next_round']}"); + } + } else {//只有小奖中奖 + // 小奖中奖记录 + $this->addWinnerRecord( + $winnerUid, + 1, // 小奖 + $result['small_prize_amount'],//中奖金额 + $result['small_total_gold'],//奖池总金额 + $this->getSmallRatio($result['small_prize_amount'], $result['small_total_gold']),//中奖比例 + 0 //释放金额 + ); + // 3. 小奖池开奖流水 + $this->addPoolFlow( + 1, // 小奖池 + 2, // 开奖扣除 + -$result['small_total_gold'], + $result['small_total_gold'], + 0, + $giftId, + $result['small_round'] - 1, // 开奖轮次为当前轮次-1(已结束的轮次) + "小奖池开奖:轮次" . ($result['small_round'] - 1).",累计{$result['small_total_gold']}金币" + ); + + } + } + + Db::commit(); + return [ + 'code' => 1, + 'msg' => '处理成功', + 'data' => $result + ]; + } catch (Exception $e) { + Db::rollback(); + throw new Exception($e->getMessage()); + } + } + + /** + * 添加奖池流水 + * @param int $pool_type 奖池类型:1-小 2-大 + * @param int $type 流水类型:1-累计 2-开奖 3-划转 4-释放 + * @param float $amount 金额 + * @param float $before_amount 操作前金额 + * @param float $after_amount 操作后金额 + * @param int $relate_id 关联ID + * @param int $times 轮次 + * @param string $remark 备注 + */ + private function addPoolFlow($pool_type, $type, $amount, $before_amount, $after_amount, $relate_id, $times, $remark) + { + Db::name('bb_lottery_pool_flow')->insert([ + 'pool_type' => $pool_type, + 'type' => $type, + 'amount' => $amount, + 'before_amount' => $before_amount, + 'after_amount' => $after_amount, + 'relate_id' => $relate_id, + 'times' => $times, // 新增:写入轮次 + 'remark' => $remark, + 'create_time' => time() + ]); + } + + /** + * 添加中奖记录 + * @param int $uid 中奖用户ID + * @param int $prize_type 奖项类型:1-小 2-大 + * @param float $prize_amount 中奖金额 + * @param float $pool_amount 奖池总金额 + * @param int $ratio 中奖比例 + * @param float $release_amount 释放金额 + */ + private function addWinnerRecord($uid, $prize_type, $prize_amount, $pool_amount, $ratio, $release_amount) + { + Db::name('bb_lottery_winner_record')->insert([ + 'uid' => $uid, + 'prize_type' => $prize_type, + 'prize_amount' => $prize_amount, + 'pool_amount' => $pool_amount, + 'ratio' => $ratio, + 'release_amount' => $release_amount, + 'create_time' => time(), + 'status' => 1 // 已发放 + ]); + // 此处可添加用户金币入账逻辑(如更新用户金币表) + } + + /** + * 计算小奖中奖比例 + * @param float $prize_amount 中奖金额 + * @param float $pool_amount 奖池金额 + * @return int 比例(%) + */ + private function getSmallRatio($prize_amount, $pool_amount) + { + return intval(round($prize_amount / $pool_amount * 100)); + } + + /** + * 计算大奖中奖比例 + * @param float $prize_amount 中奖金额 + * @param float $pool_amount 奖池金额 + * @return int 比例(%) + */ + private function getBigRatio($prize_amount, $pool_amount) + { + return intval(round($prize_amount / $pool_amount * 100)); + } + + /** + * 统计中奖数据 + * @param array $where 筛选条件(如uid、prize_type、time) + * @return array 统计结果 + */ + public function statWinner($where = []) + { + $query = Db::name('bb_lottery_winner_record'); + if (!empty($where['uid'])) { + $query->where('uid', $where['uid']); + } + if (!empty($where['prize_type'])) { + $query->where('prize_type', $where['prize_type']); + } + if (!empty($where['start_time']) && !empty($where['end_time'])) { + $query->whereBetween('create_time', [$where['start_time'], $where['end_time']]); + } + // 总中奖金额、总释放金额、中奖次数 + $stat = $query->field([ + 'SUM(prize_amount) as total_prize', + 'SUM(release_amount) as total_release', + 'COUNT(id) as total_times' + ])->find(); + return [ + 'total_prize' => $stat['total_prize'] ?? 0, + 'total_release' => $stat['total_release'] ?? 0, + 'total_times' => $stat['total_times'] ?? 0 + ]; + } +} \ No newline at end of file