redis = new Redis(); // 连接到Redis服务器 $this->redis->connect(config('redis.host'), config('redis.port')); // 根据实际配置调整主机和端口 // 选择数据库1 $this->redis->select(1); } catch (\Exception $e) { Log::record('Redis连接失败: ' . $e->getMessage(), 'error'); $this->redis = null; } } /** * 重构后的抽奖方法 - 优化响应速度 */ public function draw_gift($gift_bag_id, $user_id, $gift_user_ids, $num = 1, $room_id = 0, $heart_id = 0,$auction_id = 0) { // 最大重试次数 // $maxRetries = 3; // for ($attempt = 0; $attempt < $maxRetries; $attempt++) { // try { // 1. 验证参数并提前处理错误 $validationResult = $this->validateDrawParameters($gift_bag_id, $user_id, $gift_user_ids); if ($validationResult !== true) { return $validationResult; } // 2. 预加载必要数据 $loadResult = $this->loadDrawData($gift_bag_id, $user_id, $room_id, $num, $gift_user_ids); if ($loadResult['code'] !== 1) { return $loadResult; } // 添加以下检查以防止 null 错误 if (!isset($loadResult['data']) || !is_array($loadResult['data'])) { return ['code' => 0, 'msg' => '数据加载失败', 'data' => null]; } ['bag_data' => $bag_data, 'room' => $room, 'xlh_ext' => $xlh_ext] = $loadResult['data']; // 3. 预计算抽奖结果 $precomputeResult = $this->precomputeDrawResults( $bag_data, $gift_user_ids, $num ); if ($precomputeResult['code'] !== 1) { return $precomputeResult; } $precomputedResults = $precomputeResult['data']['results']; foreach ($precomputedResults as $key => $result) { if ($result['gift'] == null) { Log::record('数据报错-3:'.json_encode($result),"info"); } } $availableGiftss = $precomputeResult['data']['availableGifts']; $currentXlhPeriodsNum = $precomputeResult['data']['current_xlh_periods_num']; $addcurrentXlhPeriodsNum = $precomputeResult['data']['addcurrentXlhPeriodsNum']; $expectedCount = count(explode(',', $gift_user_ids)) * $num; if (count($precomputedResults) != $expectedCount) { // 记录错误到Redis $this->recordDrawErrorToRedis($expectedCount, count($precomputedResults), $room_id, $user_id, $gift_bag_id, $num, $gift_user_ids, $precomputedResults); return ['code' => 0, 'msg' => '网络加载失败,请重试!', 'data' => null]; } // 4. 执行抽奖事务(核心操作) $transactionResult = $this->executeDrawTransaction( $bag_data, $user_id, $room_id, $num, $precomputedResults, $availableGiftss, $gift_user_ids, $heart_id, $auction_id ); if ($transactionResult['code'] !== 1) { return $transactionResult; } $boxTurntableLog = $transactionResult['data']['log_id']; $giftCounts = $transactionResult['data']['gift_counts']; // 5. 处理后续操作(非事务性操作) $this->handlePostDrawOperations( $precomputedResults, $boxTurntableLog, $room_id, $xlh_ext, $currentXlhPeriodsNum, $addcurrentXlhPeriodsNum, $room ); // 6. 构建并返回结果 return $this->buildDrawResult($boxTurntableLog, $giftCounts); // } catch (\Exception $e) { // $key = 'blind_box_draw_errors_' . date('Y-m-d-H-i-s'); // $errorData = [ // 'gift_bag_id' => $gift_bag_id, // 'user_id' => $user_id, // 'gift_user_ids' => $gift_user_ids, // 'num' => $num, // 'room_id' => $room_id, // 'heart_id' => $heart_id, // 'auction_id' => $auction_id, // ]; // if ($this->redis) { // $this->redis->setex($key, 86400 * 7, $e->getMessage() . ' ' . json_encode($errorData)); // } // // 如果是死锁且还有重试机会 // if (strpos($e->getMessage(), 'Deadlock') !== false && $attempt < $maxRetries - 1) { // // 随机延迟后重试 // usleep(rand(50000, 200000)); // 50-200ms // continue; // } // return ['code' => 0, 'msg' => "网络加载失败,请重试!", 'data' => null]; // } // } } /** * 验证抽奖参数 */ private function validateDrawParameters($gift_bag_id, $user_id, $gift_user_ids) { // 提前验证收礼人 $toarray = explode(',', $gift_user_ids); if (in_array($user_id, $toarray)) { return ['code' => 0, 'msg' => "收礼人不能包含自己", 'data' => null]; } // 验证用户ID if (empty($user_id)) { return ['code' => 0, 'msg' => '用户ID不能为空', 'data' => null]; } // 验证盲盒ID if (empty($gift_bag_id)) { return ['code' => 0, 'msg' => '盲盒ID不能为空', 'data' => null]; } return true; } /** * 预加载必要数据 */ private function loadDrawData($gift_bag_id, $user_id, $room_id,$num,$gift_user_ids) { // 1. 合并查询盲盒配置和礼物信息 $bag_data = db::name("vs_gift_bag") ->alias('bag') ->join('vs_gift gift', 'gift.gid = JSON_UNQUOTE(JSON_EXTRACT(bag.ext, "$.gift_id"))', 'LEFT') ->field('bag.id,bag.name,bag.ext,gift.gid as gift_id,gift.gift_price') ->where('bag.id', $gift_bag_id) ->find(); if (!$bag_data || !is_array($bag_data) || !isset($bag_data['gift_price'])) { return ['code' => 0, 'msg' => '盲盒配置不存在或盲盒礼物不存在', 'data' => null]; } // 2. 获取房间信息 $room = db::name('vs_room') ->field('id,room_name,xlh_periods,xlh_periods_num,is_open_blind_box_turntable') ->where(['id' => $room_id]) ->find(); if (!$room) { return ['code' => 0, 'msg' => '房间不存在', 'data' => null]; } // 3. 检查用户金币 $user_waller = db::name('user_wallet') ->where(['user_id' => $user_id]) ->find(); if (!$user_waller) { return ['code' => 0, 'msg' => '用户钱包不存在', 'data' => null]; } if ($user_waller['coin'] < $bag_data['gift_price'] * $num * count(explode(',', $gift_user_ids))) { return ['code' => 0, 'msg' => '用户金币不足', 'data' => null]; } // 4. 获取巡乐会配置(使用缓存) $xlh_ext = $this->getCachedXlhConfig(); return [ 'code' => 1, 'msg' => '数据加载成功', 'data' => [ 'bag_data' => $bag_data, 'room' => $room, 'xlh_ext' => $xlh_ext, // 'user_waller' => $user_waller ] ]; } /** * 预计算抽奖结果 */ private function precomputeDrawResults($bag_data, $gift_user_ids, $num) { $toarray = explode(',', $gift_user_ids); // 1. 计算奖池信息 $poolInfo = $this->calculatePoolInfo($bag_data['id']); if ($poolInfo['code'] !== 1) { return $poolInfo; } $totalRemaining = $poolInfo['data']['total_remaining'];//奖池剩余数量 $periods = $poolInfo['data']['periods']; //奖池期数 $totalDrawTimes = $poolInfo['data']['total_draw_times']; //总抽奖次数 // 2. 获取可用礼物 $availableGifts = $this->getAvailableGifts($bag_data['id'], $totalDrawTimes); if (empty($availableGifts)) { $availableGifts = $this->resetPoolAndReload($bag_data['id']); //重置奖池并重新加载 if (empty($availableGifts)) { throw new \Exception('重置奖池后仍无可用礼物'); } $totalDrawTimes = 0;//总抽奖次数重置 } // 在处理奖池重置逻辑之前添加检查 if (!is_array($availableGifts)) { throw new \Exception('可用礼物数据格式错误'); } // 3. 预加载礼物信息(减少后续查询) $giftInfoMap = $this->preloadGiftInfo($availableGifts); // 4. 处理奖池重置逻辑 - 优化版本 $needGiftNum = count($toarray) * $num; //总需要的礼物数 $remaining_available_gifts = []; $remainingGiftCount = array_sum(array_column($availableGifts, 'remaining_number')); if ($remainingGiftCount < $needGiftNum) { // 如果当前奖池礼物不足以满足需求 // 保存当前剩余礼物作为上期剩余 $remaining_available_gifts = $availableGifts; // 重置奖池 $availableGifts = $this->resetPoolAndReload($bag_data['id']); if (empty($availableGifts)) { throw new \Exception('重置奖池后仍无可用礼物'); } $totalDrawTimes = 0; // 总抽奖次数重置 } $current_xlh_periods_num = $this->getCachedXlhPeriodsNum("get"); // 5. 使用Alias Method预计算抽奖结果(O(1)复杂度) $precomputedResults = $this->precomputeResultsWithAliasMethod( $toarray, $num, $availableGifts, $giftInfoMap, $totalDrawTimes, $periods, $current_xlh_periods_num, $remaining_available_gifts ); if (empty($precomputedResults['precomputedResults'])) { throw new \Exception('预计算抽奖结果失败'); } return [ 'code' => 1, 'msg' => '预计算成功', 'data' => [ 'results' => $precomputedResults['precomputedResults'], 'current_xlh_periods_num' => $current_xlh_periods_num, 'availableGifts' => $precomputedResults['precomputedResultss'], 'addcurrentXlhPeriodsNum' => $precomputedResults['addcurrentXlhPeriodsNum'], ] ]; } /** * 使用Alias Method预计算抽奖结果(O(1)复杂度) */ private function precomputeResultsWithAliasMethod( $toarray, $num, $availableGifts, $giftInfoMap, &$totalDrawTimes, &$periods, &$currentXlhPeriodsNum, $remaining_available_gifts ) { if (!is_array($toarray) || !is_array($availableGifts) || !is_array($giftInfoMap)) { throw new \Exception('预计算参数格式错误'); } $precomputedResults = []; $precomputedResultss = []; $addcurrentXlhPeriodsNum = 0; // 计算上期剩余礼物总数 $remainingGiftCount = array_sum(array_column($remaining_available_gifts, 'remaining_number')); $needGiftNum = count($toarray) * $num; $newGiftsNeeded = max(0, $needGiftNum - $remainingGiftCount); // 计算还需要多少礼物从新奖池中抽取 // 先从上期剩余礼物中分配 if (!empty($remaining_available_gifts)) { $aliasTableForRemaining = $this->buildAliasTable($remaining_available_gifts); foreach ($toarray as $giftUserId) { // 为每个用户先分配上期剩余礼物 $userRemainingAllocation = floor($remainingGiftCount / count($toarray)); if (count($toarray) > 0) { // 防止除零错误 $extraGifts = $remainingGiftCount % count($toarray); if (array_search($giftUserId, $toarray) < $extraGifts) { $userRemainingAllocation++; } } for ($i = 0; $i < $userRemainingAllocation; $i++) { $selectedGift = $this->selectGiftWithAliasMethod($aliasTableForRemaining); if ($selectedGift) { $gift = $giftInfoMap[$selectedGift['foreign_id']]; $precomputedResults[] = [ 'gift_user_id' => $giftUserId, 'gift_bag_detail' => $selectedGift, 'gift' => $gift, 'draw_times' => $totalDrawTimes, 'periods' => $periods, ]; $precomputedResultss[] = [ 'gift_user_id' => $giftUserId, 'gift_bag_detail' => $selectedGift, 'gift' => $gift, 'draw_times' => $totalDrawTimes, 'periods' => $periods, ]; $totalDrawTimes++; $currentXlhPeriodsNum++; $addcurrentXlhPeriodsNum++; // 更新Alias表 $this->updateAliasTable($aliasTableForRemaining, $selectedGift['id']); } } } } // 再从新奖池中分配剩余所需礼物 if ($newGiftsNeeded > 0 && !empty($availableGifts)) { $aliasTableForNew = $this->buildAliasTable($availableGifts); foreach ($toarray as $giftUserId) { // 计算每个用户需要从新奖池获得的礼物数量 $userNewAllocation = floor($newGiftsNeeded / count($toarray)); if (count($toarray) > 0) { $extraGifts = $newGiftsNeeded % count($toarray); if (array_search($giftUserId, $toarray) < $extraGifts) { $userNewAllocation++; } } for ($i = 0; $i < $userNewAllocation; $i++) { $selectedGift = $this->selectGiftWithAliasMethod($aliasTableForNew); if ($selectedGift) { $gift = $giftInfoMap[$selectedGift['foreign_id']] ?? null; $precomputedResults[] = [ 'gift_user_id' => $giftUserId, 'gift_bag_detail' => $selectedGift, 'gift' => $gift, 'draw_times' => $totalDrawTimes, 'periods' => $periods, ]; $precomputedResultss[] = [ 'gift_user_id' => $giftUserId, 'gift_bag_detail' => $selectedGift, 'gift' => $gift, 'draw_times' => $totalDrawTimes, 'periods' => $periods, ]; $totalDrawTimes++; $currentXlhPeriodsNum++; $addcurrentXlhPeriodsNum++; // 更新Alias表 $this->updateAliasTable($aliasTableForNew, $selectedGift['id']); } } } } return ['precomputedResults' => $precomputedResults, 'precomputedResultss' => $precomputedResultss, 'addcurrentXlhPeriodsNum' => $addcurrentXlhPeriodsNum]; } /** * 构建Alias表(O(n)复杂度,只执行一次) */ private function buildAliasTable($gifts) { $n = count($gifts); if ($n === 0) return null; $totalRemaining = array_sum(array_column($gifts, 'remaining_number')); if ($totalRemaining <= 0) return null; // 初始化Alias表 $small = []; $large = []; $prob = []; $alias = []; $indexMap = []; // 归一化概率并填充索引映射 foreach ($gifts as $i => $gift) { $indexMap[$i] = $gift; $prob[$i] = $gift['remaining_number'] * $n / $totalRemaining; if ($prob[$i] < 1.0) { $small[] = $i; } else { $large[] = $i; } } // 构建Alias表 while (!empty($small) && !empty($large)) { $s = array_pop($small); $l = array_pop($large); $alias[$s] = $l; $prob[$l] = ($prob[$l] + $prob[$s]) - 1.0; if ($prob[$l] < 1.0) { $small[] = $l; } else { $large[] = $l; } } return [ 'n' => $n, 'prob' => $prob, 'alias' => $alias, 'index_map' => $indexMap, 'gifts' => $gifts ]; } /** * 使用Alias Method选择礼物(O(1)复杂度) */ private function selectGiftWithAliasMethod($aliasTable) { if (!$aliasTable) return null; $n = $aliasTable['n']; $k = mt_rand(0, $n - 1); // 随机选择 if (mt_rand() / mt_getrandmax() < $aliasTable['prob'][$k]) { return $aliasTable['index_map'][$k]; } else { return $aliasTable['index_map'][$aliasTable['alias'][$k]] ?? null; } } /** * 更新Alias表(模拟库存减少) */ private function updateAliasTable(&$aliasTable, $giftId) { // 查找礼物在Alias表中的位置 $gifts = &$aliasTable['gifts']; $indexMap = &$aliasTable['index_map']; foreach ($gifts as &$gift) { if ($gift['id'] == $giftId) { $gift['remaining_number']--; break; } } // 重新构建Alias表(当剩余数量变化较大时) // 这里可以根据实际情况调整重建频率 $totalRemaining = array_sum(array_column($gifts, 'remaining_number')); if ($totalRemaining <= 0) { $aliasTable = null; } } /** * 执行抽奖事务(核心操作) */ private function executeDrawTransaction($bag_data, $user_id, $room_id, $num, $precomputedResults,$availableGiftss,$gift_user_ids,$heart_id,$auction_id) { $gift_user_num = count(explode(',', $gift_user_ids)); //人数 $bagGiftPrice = $bag_data['gift_price'] * $num * $gift_user_num; // 增加重试机制 // $maxRetries = 3; // for ($retry = 0; $retry < $maxRetries; $retry++) { // try { db::startTrans(); // 按照固定顺序处理事务步骤 // 1. 扣除用户金币(优先处理) $this->deductUserCoins($user_id, $bagGiftPrice, $room_id); // 2. 创建抽奖记录 $boxTurntableLog = db::name('vs_blind_box_turntable_log')->insertGetId([ 'user_id' => $user_id, 'gift_bag_id' => $bag_data['id'], 'num' => $num, 'room_id' => $room_id, 'bag_price' => $bag_data['gift_price'], 'createtime' => time() ]); if (!$boxTurntableLog) { throw new \Exception('添加盲盒转盘记录失败'); } // 3. 批量更新库存(按ID排序避免死锁) $this->batchUpdateGiftInventory($availableGiftss, $room_id); // 4. 批量插入礼包发放记录 $this->batchInsertGiftBagReceiveLog($user_id, $boxTurntableLog, $bag_data, $room_id, $precomputedResults); // 5. 发送礼物 $result = $this->sendGiftsToRecipients($precomputedResults, $room_id,$user_id,$heart_id,$auction_id); if (isset($result['code']) && $result['code'] !== 1) { throw new \Exception($result['msg']); } db::commit(); // 统计礼物数量 $giftCounts = $this->countGifts($precomputedResults); return [ 'code' => 1, 'msg' => '事务执行成功', 'data' => [ 'log_id' => $boxTurntableLog, 'gift_counts' => $giftCounts ] ]; // } catch (\Exception $e) { // db::rollback(); // // 检查是否是死锁错误 // if (strpos($e->getMessage(), 'Deadlock') !== false && $retry < $maxRetries - 1) { // // 等待随机时间后重试 // usleep(rand(10000, 100000)); // 10-100ms // continue; // } // return ['code' => 0, 'msg' => $e->getMessage(), 'data' => null]; // } // } return ['code' => 0, 'msg' => '操作超时,请重试', 'data' => null]; } /** * 批量更新礼物库存 */ private function batchUpdateGiftInventory($precomputedResults, $room_id) { // 按礼物ID分组统计需要减少的数量 $inventoryUpdates = []; foreach ($precomputedResults as $result) { $giftId = $result['gift_bag_detail']['id']??0; $inventoryUpdates[$giftId] = ($inventoryUpdates[$giftId] ?? 0) + 1; } // 按ID排序避免死锁 ksort($inventoryUpdates); // 批量更新 foreach ($inventoryUpdates as $giftId => $count) { $ret = db::name("vs_gift_bag_detail")->where('id',$giftId) ->setDec('remaining_number', $count); if (!$ret) { Log::record('巡乐会更新礼物剩余数量: ' . $room_id."【数据】".var_export($precomputedResults, true),"info"); throw new \Exception('更新礼物剩余数量失败'); } } } /** * 批量插入礼包发放记录 */ private function batchInsertGiftBagReceiveLog($user_id, $boxTurntableLog, $bag_data, $room_id, $precomputedResults) { $batchInsertData = []; foreach ($precomputedResults as $result) { if(!isset($result['gift']['gift_price'])){ Log::record('数据报错:'.json_encode($result),"info"); } $batchInsertData[] = [ 'user_id' => $user_id, 'gift_user_id' => $result['gift_user_id'], 'parent_id' => $boxTurntableLog, 'gift_bag_id' => $bag_data['id'], 'gift_id' => $result['gift_bag_detail']['foreign_id'], 'periods' => $result['periods'], 'room_id' => $room_id, 'gift_price' => $result['gift']['gift_price'], 'bag_price' => $bag_data['gift_price'], 'createtime' => time() ]; } if (!empty($batchInsertData)) { $insertResult = db::name("vs_gift_bag_receive_pan_log")->insertAll($batchInsertData); if (!$insertResult) { throw new \Exception('插入礼包发放记录失败'); } } } /** * 扣除用户金币 */ private function deductUserCoins($user_id, $bagGiftPrice, $room_id) { // 使用悲观锁查询用户钱包 $userWallet = db::name('user_wallet') ->where(['user_id' => $user_id]) ->find(); if (!$userWallet || $userWallet['coin'] < $bagGiftPrice) { throw new \Exception('用户金币不足'); } $walletUpdate = model('GiveGift')->change_user_cion_or_earnings_log( $user_id, $bagGiftPrice, $room_id, 1, 10, '盲盒转盘抽奖消耗' ); if (!$walletUpdate) { throw new \Exception('扣除用户金币失败'); } $userLevel = model('Level')->user_level_data_update( $user_id, $bagGiftPrice, 1, $room_id ); if (!$userLevel) { throw new \Exception('用户等级更新失败'); } } /** * 处理抽奖后的后续操作(非事务性) */ private function handlePostDrawOperations( $precomputedResults, $boxTurntableLog, $room_id, $xlh_ext, $currentXlhPeriodsNum, $addcurrentXlhPeriodsNum, $room ) { // 1. 批量插入盲盒转盘结果记录 $this->batchInsertBlindBoxResults($precomputedResults, $boxTurntableLog); // 3. 处理巡乐会相关操作 if (!empty($xlh_ext) && $xlh_ext['inlet_bag_id'] == $precomputedResults[0]['gift_bag_detail']['gift_bag_id']) { $this->handleXlhOperations($room_id, $xlh_ext, $currentXlhPeriodsNum,$addcurrentXlhPeriodsNum,$room); } } /** * 批量插入盲盒转盘结果记录 */ private function batchInsertBlindBoxResults($precomputedResults, $boxTurntableLog) { // 统计每个用户每个礼物的数量 $giftUserCounts = []; foreach ($precomputedResults as $result) { $key = $result['gift_user_id'] . '_' . $result['gift_bag_detail']['foreign_id']; if (!isset($giftUserCounts[$key])) { $giftUserCounts[$key] = [ 'gift_user_id' => $result['gift_user_id'], 'gift_id' => $result['gift_bag_detail']['foreign_id'], 'count' => 0, 'gift_price' => $result['gift']['gift_price'] ]; } $giftUserCounts[$key]['count']++; } // 批量插入 $batchInsertData = []; foreach ($giftUserCounts as $userGift) { $batchInsertData[] = [ 'tid' => $boxTurntableLog, 'gift_user_id' => $userGift['gift_user_id'], 'gift_id' => $userGift['gift_id'], 'count' => $userGift['count'], 'gift_price' => $userGift['gift_price'], 'all_gift_price' => $userGift['gift_price'] * $userGift['count'], 'createtime' => time(), 'heart_id' => 0 ]; } if (!empty($batchInsertData)) { db::name('vs_blind_box_turntable_results_log')->insertAll($batchInsertData); } } /** * 发送礼物给接收者 */ private function sendGiftsToRecipients($precomputedResults, $room_id,$user_id,$heart_id,$auction_id) { // 统计每个用户每个礼物的数量 $giftUserCounts = []; foreach ($precomputedResults as $result) { $key = $result['gift_user_id'] . '_' . $result['gift_bag_detail']['foreign_id']; if (!isset($giftUserCounts[$key])) { $giftUserCounts[$key] = [ 'gift_user_id' => $result['gift_user_id'], 'gift_id' => $result['gift_bag_detail']['foreign_id'], 'count' => 0, 'gift_price' => $result['gift']['gift_price'] ]; } $giftUserCounts[$key]['count']++; } // 批量发送礼物 foreach ($giftUserCounts as $userGift) { if($userGift['count'] > 9){ //防止礼物超发,礼物超10个则不发送 continue; } $giveGiftExt = [ 'gift_id' => $userGift['gift_id'], 'count' => $userGift['count'], 'gift_price' => $userGift['gift_price'], 'all_gift_price' => $userGift['gift_price'] * $userGift['count'], 'is_draw_gift' => 1 ]; if(!empty($auction_id)){ model('RoomAuction')->room_auction_join($auction_id,$user_id,$userGift['gift_id'],$userGift['count'],2,$giveGiftExt); }else{ $res = model('Room')->room_gift( $user_id, $userGift['gift_user_id'], $userGift['gift_id'], $userGift['count'], 1, $room_id, 0, $heart_id, $giveGiftExt ); } if (isset($res) && $res['code'] != 1) { Log::record('发送礼物失败: ' . $res['msg'] . $userGift['gift_user_id'], "info"); return ['code' => 0, 'msg' => $res['msg'], 'data' => null]; } } return ['code' => 1, 'msg' => '发送礼物成功', 'data' => null]; } /** * 处理巡乐会相关操作 */ private function handleXlhOperations($room_id, $xlh_ext, $currentXlhPeriodsNum,$addcurrentXlhPeriodsNum,$room) { $xlhIsPiaoPing = 0; $xlhPeriodsNum = $this->getCachedXlhPeriodsNum("get"); if($xlhPeriodsNum < $xlh_ext['open_condition']['waiting_start_num'] && $currentXlhPeriodsNum >= $xlh_ext['open_condition']['waiting_start_num']){ $xlhIsPiaoPing = 1; } if($xlhPeriodsNum < $xlh_ext['open_condition']['start_num'] && $currentXlhPeriodsNum >= $xlh_ext['open_condition']['start_num']){ $xlhIsPiaoPing = 2; } // 更新房间巡乐会次数 $this->getCachedXlhPeriodsNum("set",$addcurrentXlhPeriodsNum); // 处理飘屏 if ($xlhIsPiaoPing == 1 || $xlhIsPiaoPing == 2) { $this->handleXlhPiaoPing($room_id, $xlh_ext, $xlhIsPiaoPing); } $this->updateAndPushXlhStatus($room_id, $xlh_ext, $currentXlhPeriodsNum); } private function handleXlhPiaoPing($room_id, $xlh_ext, $xlhIsPiaoPing){ if($xlhIsPiaoPing == 1){ // 即将开始推送飘屏 $text = "巡乐会即将开始..."; // 推送礼物横幅 $push = new Push(UID, $room_id); $text_list_new = [ 'text' => $text, 'room_id' => $room_id, 'from_type' => 101 ]; $push->xunlehui($text_list_new); } if($xlhIsPiaoPing == 2){ // 正式开始推送飘屏 $text = "巡乐会游戏已正式开启..."; // 推送礼物横幅 $push = new Push(UID, $room_id); $text_list_new = [ 'text' => $text, 'room_id' => $room_id, 'from_type' => 102 ]; $push->xunlehui($text_list_new); // 巡乐会正式开始 $this_xlh_periods = $this->getCachedXlhPeriods('get'); $pan_xlh_id = db::name('vs_room_pan_xlh')->insertGetId([ 'room_id' => $room_id, 'gift_id' => $xlh_ext['locking_condition']['locking_gift_id'], 'homeowner_gift_id' => $xlh_ext['locking_condition']['give_homeowner_gift_id'], 'periods' => $this_xlh_periods+1, 'num' => 0, 'end_time' => time() + $xlh_ext['locking_time']['end_time'] * 60, 'createtime' => time() ]); if(!$pan_xlh_id){ return ['code' => 0, 'msg' => '创建巡乐会失败!', 'data' => []]; } $this->getCachedXlhPeriods('set', $this_xlh_periods+1);//修改巡乐会期数 } } private function updateAndPushXlhStatus($room_id, $xlh_ext, $currentXlhPeriodsNum){ $xlh['waiting_start_num'] = $xlh_ext['open_condition']['waiting_start_num'];//等待开奖次数 $xlh['start_num'] = $xlh_ext['open_condition']['start_num'];//开始开奖次数 // 当前抽奖次数 $xlh['current_num'] = $currentXlhPeriodsNum; $xlh['end_time'] = 0; // 状态 if($xlh['current_num'] >= $xlh_ext['open_condition']['start_num']){ $xlh['status'] = 1;//状态 1:巡乐会开始 2:即将开始开始 0:等待开始 //查询巡乐会信息 $pan_xlh = db::name('vs_room_pan_xlh')->where('send_time',0)->order('id desc')->find(); if(empty($pan_xlh)){ $this->handleXlhPiaoPing($room_id, $xlh_ext, 2); $pan_xlh = db::name('vs_room_pan_xlh')->where('send_time',0)->order('id desc')->find(); } $xlh['end_time'] = $pan_xlh['end_time'] ?? 0; } elseif($xlh['current_num'] >= $xlh_ext['open_condition']['waiting_start_num'] && $xlh['current_num'] < $xlh_ext['open_condition']['start_num']){ $xlh['status'] = 2;//状态 1:巡乐会开始 2:即将开始开始 0:等待开始 }else{ $xlh['status'] = 0; } // 推送进度条 $push = new Push(UID, $room_id); $text_list_new = [ 'xlh_data' => $xlh, 'text' => "", 'room_id' => $room_id, 'from_type' => 100 ]; $push->xunlehui($text_list_new); // 聊天室推送系统消息 // 推送 // $text = [ // 'xlh_data' => $xlh, // 'text' => "" // ]; // model('Chat')->sendMsg(1056,$room_id,$text); } /** * 统计礼物数量 */ private function countGifts($precomputedResults) { $giftCounts = []; foreach ($precomputedResults as $result) { $giftId = $result['gift_bag_detail']['foreign_id']; if (!isset($giftCounts[$giftId])) { $giftCounts[$giftId] = [ 'gift_id' => $giftId, 'count' => 0, 'gift_price' => $result['gift']['gift_price'] ]; } $giftCounts[$giftId]['count']++; } return $giftCounts; } /** * 构建抽奖结果 */ private function buildDrawResult($boxTurntableLog, $giftCounts) { $resultList = []; foreach ($giftCounts as $gift) { $resultList[] = [ 'gift_id' => $gift['gift_id'], 'count' => $gift['count'] ]; } return [ 'code' => 1, 'msg' => '成功', 'data' => [ 'blind_box_turntable_id' => $boxTurntableLog, 'reslut_list' => $resultList ] ]; } /** * 计算奖池信息 */ private function calculatePoolInfo($gift_bag_id) { $roomPanInfo = db::name("vs_gift_bag_detail") ->where(['gift_bag_id' => $gift_bag_id]) ->field('SUM(quantity) as total_quantity, SUM(remaining_number) as total_remaining') ->find(); $periods = db::name("vs_gift_bag") ->where(['id' => $gift_bag_id]) ->value('periods'); $totalQuantity = $roomPanInfo['total_quantity'] ?: 0; $totalRemaining = $roomPanInfo['total_remaining'] ?: 0; $periods = $periods ?: 0; $totalDrawTimes = max(0, $totalQuantity - $totalRemaining); //总抽奖次数 return [ 'code' => 1, 'msg' => '计算成功', 'data' => [ 'total_quantity' => $totalQuantity, 'total_remaining' => $totalRemaining, 'periods' => $periods, 'total_draw_times' => $totalDrawTimes ] ]; } /** * 获取可用礼物 * @param $gift_bag_id * @param $total_draw_times 总抽奖次数 */ private function getAvailableGifts($gift_bag_id, $total_draw_times = 0) { $where = [ 'gift_bag_id' => $gift_bag_id, 'quantity' => ['>', 0], 'remaining_number' => ['>', 0], 'weight' => ['<=', $total_draw_times], ]; return db::name("vs_gift_bag_detail") ->field('id,quantity,remaining_number,weight,foreign_id,gift_bag_id') ->where($where) ->select(); } /** * 预加载礼物信息 */ private function preloadGiftInfo($available_gifts) { $giftIds = array_unique(array_column($available_gifts, 'foreign_id')); if (empty($giftIds)) return []; $gifts = db::name('vs_gift') ->where('gid', 'in', $giftIds) ->select(); return array_column($gifts, null, 'gid'); } /** * 重置奖池并重新加载 */ private function resetPoolAndReload($gift_bag_id) { $this->reset_gift_pool($gift_bag_id); return $this->getAvailableGifts($gift_bag_id); } /** * 获取缓存的巡乐会配置 */ private function getCachedXlhConfig() { $cacheKey = "xlh_config_13"; $ext = Cache::get($cacheKey); if (!$ext) { $bag_data = db::name("vs_gift_bag") ->field('id,name,ext,periods') ->where('id', 13) ->find(); if (!$bag_data) { return []; } $ext = json_decode($bag_data['ext'], true); $ext['gift_bag_name'] = $bag_data['name']; Cache::set($cacheKey, $ext, 3600); // 缓存1小时 } return $ext; } /** * 获取缓存的巡乐会开启次数 */ private function getCachedXlhPeriodsNum($type="get",$num=1) { $cacheKey = "xlh_periods_num"; $xlh_periods_num = Cache::get($cacheKey) ?? 0; if($type=="set"){ $xlh_periods_num = $xlh_periods_num + $num; Cache::set($cacheKey, $xlh_periods_num, 0); } return $xlh_periods_num; } /** * 获取缓存的巡乐会期数 */ private function getCachedXlhPeriods($type="get",$periods=1) { $cacheKey = "this_xlh_periods"; $xlh_periods_num = Cache::get($cacheKey) ?? 0; if($type=="set"){ $xlh_periods_num = $periods; Cache::set($cacheKey, $xlh_periods_num, 0); } return $xlh_periods_num; } /** * 重置奖池 * @param int $room_id 房间ID * @param int $gift_bag_id 礼物包ID */ private function reset_gift_pool($gift_bag_id,$remaining_available_gifts=[]) { $bag_detail = db::name("vs_gift_bag_detail")->where('gift_bag_id',$gift_bag_id)->select(); db::name("vs_gift_bag")->where('id',$gift_bag_id)->setInc('periods'); //更新期数 db::name("vs_gift_bag_detail")->where('gift_bag_id',$gift_bag_id)->update(['remaining_number'=>db::raw('quantity')]);//重置奖池 //防止并发,上把如果件数小于0,则加上 foreach ($bag_detail as $pan) { if($pan['remaining_number']<0){ db::name("vs_gift_bag_detail")->where('id', $pan['id'])->setInc('remaining_number', $pan['remaining_number']); } } //补充上把礼物有剩余 if(!empty($remaining_available_gifts)){ foreach ($remaining_available_gifts as $gift) { db::name("vs_gift_bag_detail")->where('id', $gift['id'])->setInc('remaining_number',$gift['remaining_number']); } } } /* * 巡乐会抽奖----------------------------------------------------------------------------------------- */ /* * 巡乐会抽奖(优化版) */ public function xlh_draw_gift($user_id, $num, $room_id) { $gift_bag_id = 13; // 1. 获取并缓存盲盒配置 $ext = $this->getCachedXlhConfig(); // 2. 检查用户金币和房间状态 $bag_gift_price = $ext['xlh_box_price'] * $num; // 3. 检查巡乐会状态 $pan_xlh = db::name('vs_room_pan_xlh') ->where(['send_time' => 0]) ->order('id', 'desc') ->find(); if (empty($pan_xlh)) { return ['code' => 0, 'msg' => '本轮未开始', 'data' => null]; } if ($pan_xlh['end_time'] <= time()) { return ['code' => 0, 'msg' => '本轮已结束', 'data' => null]; } // 4. 预加载必要数据 $gift_bag_detail = db::name("vs_gift_bag_detail") ->field('id,quantity,remaining_number,weight,foreign_id,gift_bag_id') ->where(['gift_bag_id'=>$gift_bag_id]) ->where(['remaining_number' => ['>', 0]]) ->select(); // 计算总数量和剩余数量 $total_quantity = db::name("vs_gift_bag_detail") ->where(['gift_bag_id' => $gift_bag_id]) ->sum('quantity'); $total_remaining = array_sum(array_column($gift_bag_detail, 'remaining_number')); $total_draw_times = $total_quantity - $total_remaining; // 已抽奖次数 if ($total_draw_times < 0) $total_draw_times = 0; // 5. 获取可用礼物(提前过滤无效礼物) $available_gifts = []; foreach ($gift_bag_detail as $pan) { if ($pan['remaining_number'] > 0 && $pan['weight'] <= $total_draw_times) { $available_gifts[] = $pan; } } if (empty($available_gifts)) { // 移除已无剩余数量的礼物 $remaining_available_gifts = array_filter($gift_bag_detail, function($gift) { return $gift['remaining_number'] > 0; }); $this->reset_gift_pool($gift_bag_id,$remaining_available_gifts); $available_gifts = $this->getAvailableGifts($gift_bag_id); } // 6. 预计算抽奖结果 $drawn_gifts = []; // 用于统计抽中结果 $main_prize_updates = []; // 用于记录主奖品更新 $is_zhong_jiang = 0; $pan_xlh_num = $pan_xlh['num']; // 批量处理配置 $batch_size = min(10, $num); // 每批次处理10次 $total_processed = 0; $all_results = []; // 存储所有抽奖结果 try { db::startTrans(); while ($total_processed < $num) { $current_batch = min($batch_size, $num - $total_processed); // 当前批次处理数量 // 批量扣除金币(只在第一次事务中处理) if ($total_processed == 0) { $user_waller = db::name('user_wallet')->where(['user_id' => $user_id])->find(); if (!$user_waller || $user_waller['coin'] < $bag_gift_price) { return ['code' => 0, 'msg' => '用户金币不足', 'data' => null]; } $wallet_update = model('GiveGift')->change_user_cion_or_earnings_log( $user_id, $bag_gift_price, $room_id, 1, 10, $ext['gift_bag_name'].'抽奖消耗' ); if (!$wallet_update) { throw new \Exception('扣除用户金币失败'); } $user_level = model('Level')->user_level_data_update( $user_id, $bag_gift_price, 1, $room_id ); if (!$user_level) { throw new \Exception('用户等级更新失败'); } } // 处理当前批次的抽奖 $inventory_updates = []; // 用于记录库存变化 $remaining_available_gifts = $available_gifts; for ($i = 0; $i < $current_batch; $i++) { // 从可用礼物中选择 $selected_gift = $this->selectGiftFromAvailable($available_gifts); if (!$selected_gift) { $gift_bag_detail = $this->resetPoolAndReload($gift_bag_id); $selected_gift = $this->selectGiftFromAvailable($gift_bag_detail); if(!$selected_gift){ throw new \Exception('预计算抽奖失败,重置后无可用礼物'); } } // 记录库存变化 $inventory_updates[$selected_gift['id']] = ($inventory_updates[$selected_gift['id']] ?? 0) + 1; // 记录抽中结果 $gift_id = $selected_gift['foreign_id']; $drawn_gifts[$gift_id] = ($drawn_gifts[$gift_id] ?? 0) + 1; // 处理主奖品 if ($gift_id == $ext['locking_condition']['selected_gift_id']) { $pan_xlh_num++; $main_prize_updates[] = [ 'num' => $pan_xlh_num, 'user_id' => $user_id, 'gift_id' => $gift_id ]; // 计算延长时间 $add_end_time = $this->calculateEndTime($pan_xlh_num, $ext, $room_id); $end_time = time() + $add_end_time; // 记录主奖品更新 $main_prize_updates[count($main_prize_updates) - 1]['end_time'] = $end_time; } // 记录完整结果 $all_results[] = [ 'gift_id' => $gift_id, 'gift_detail_id' => $selected_gift['id'], 'gift_bag_detail' => $selected_gift ]; // 更新可用礼物缓存 foreach ($available_gifts as &$gift) { if ($gift['id'] == $selected_gift['id']) { $gift['remaining_number']--; break; } } unset($gift); // 移除已无剩余数量的礼物 $available_gifts = array_filter($available_gifts, function($gift) { return $gift['remaining_number'] > 0; }); // 检查是否需要重置奖池 $total_remaining = array_sum(array_column($available_gifts, 'remaining_number')); if ($total_remaining <= 0 && $total_processed + $i + 1 < $num) {// 剩余数量小于等于0且当前处理数量小于总数量 $available_gifts = $this->resetPoolAndReload($gift_bag_id); if (empty($available_gifts)) { throw new \Exception('重置奖池后仍无可用礼物'); } } } // 批量更新库存 ksort($inventory_updates); // 按ID排序 foreach ($inventory_updates as $detail_id => $count) { db::name("vs_gift_bag_detail")->where('id',$detail_id)->setDec('remaining_number', $count); } // 处理主奖品更新 if (!empty($main_prize_updates)) { $last_update = end($main_prize_updates); db::name('vs_room_pan_xlh')->where('id', $pan_xlh['id'])->update([ 'user_id' => $last_update['user_id'], 'room_id' => $room_id, 'pay_price' => $ext['xlh_box_price'], 'locking_gift_id' => $last_update['gift_id'], 'num' => $last_update['num'], 'end_time' => $last_update['end_time'], 'updatetime' => time() ]); db::name('vs_room_pan_xlh_log')->insert([ 'xlh_id' => $pan_xlh['id'], 'user_id' => $last_update['user_id'], 'room_id' => $room_id, 'num' => $last_update['num'], 'locking_end_time' => $last_update['end_time'], 'createtime' => time() ]); $is_zhong_jiang = 1; }else{ $is_zhong_jiang = 0; } $total_processed += $current_batch; } // 7. 批量处理结果记录 if(count($drawn_gifts) > $num){ $key = 'xlh_draw_gift_errors_' . date('Y-m-d-H-i-s'); $errorData = [ 'user_id' => $user_id, 'gift_bag_id' => $gift_bag_id, 'room_id' => $room_id, 'num' => $num, 'drawn_gifts_num' => count($drawn_gifts) ]; $this->redis->setex($key, 86400 * 7, "巡乐会抽奖失败-数量超限". ' ' .json_encode($errorData)); return ['code' => 0, 'msg' => "抽奖中,请稍等...", 'data' => null]; } // 批量插入礼包发放记录 $gift_records = []; $periods = $this->getCachedXlhPeriods('get') ?? 0; foreach ($drawn_gifts as $gift_id => $count) { $gift_records[] = [ 'user_id' => $user_id, 'parent_id' => $pan_xlh['id'], 'gift_bag_id' => $gift_bag_id, 'gift_id' => $gift_id, 'periods' => $periods, 'room_id' => $room_id, 'num' => $count, 'gift_price' => $this->getGiftPrice($gift_id), 'bag_price' => $ext['xlh_box_price'], 'createtime' => time() ]; } if (!empty($gift_records)) { db::name("vs_gift_bag_receive_pan_log")->insertAll($gift_records); } // 批量处理用户礼物包 foreach ($drawn_gifts as $gift_id => $count) { $res = model('UserGiftPack')->change_user_gift_pack( $user_id, $gift_id, $count, model('UserGiftPack')::XLH_DRAW_GIFT_GET, "巡乐会抽奖所得" ); if ($res['code'] != 1) { throw new \Exception($res['msg']); } } // 添加活动记录 Db::name('vs_activities_receive')->insert([ 'user_id' => $user_id, 'activities_id' => 6, 'room_id' => $room_id, 'createtime' => time(), 'updatetime' => time() ]); db::commit(); } catch (\Exception $e) { db::rollback(); $key = 'xlh_draw_gift_errors_' . date('Y-m-d-H-i-s'); $errorData = [ 'user_id' => $user_id, 'gift_bag_id' => $gift_bag_id, 'room_id' => $room_id, ]; $this->redis->setex($key, 86400 * 7, $e->getMessage(). ' ' .json_encode($errorData)); return ['code' => 0, 'msg' => "抽奖中,请稍等...", 'data' => null]; } // 8. 处理推送消息 if ($is_zhong_jiang == 1) { $this->handlePrizeNotification($user_id,$gift_id, $room_id, $pan_xlh_num, $end_time,$ext['locking_condition']['locking_gift_id']); } // 9. 构建返回结果 return $this->buildResult($drawn_gifts); } /** * 巡乐会抽奖-从可用礼物中选择一个 */ private function selectGiftFromAvailable(array &$available_gifts) { $remaining = array_sum(array_column($available_gifts, 'remaining_number')); if ($remaining <= 0) { return null; } // 循环尝试直到抽中有效礼物 $max_attempts = 5; // 最大尝试次数,防止无限循环 $attempt = 0; $selected_gift = null; while ($attempt < $max_attempts && !$selected_gift) { $rand_value = mt_rand(1, $remaining); $current_sum = 0; foreach ($available_gifts as $gift) { if ($gift['remaining_number'] <= 0) { continue; } $current_sum += $gift['remaining_number']; if ($rand_value <= $current_sum) { $selected_gift = $gift; break; } } $attempt++; } return $selected_gift; } /** * 巡乐会抽奖-计算结束时间 */ private function calculateEndTime($pan_xlh_num, $ext, $room_id) { $cache_key = 'selected_gift_id_' . $ext['locking_condition']['selected_gift_id']; if ($pan_xlh_num <= 1) { $add_end_time = $ext['locking_time']['tow_no_locking_time'] * 60; Cache::set($cache_key, $add_end_time, $add_end_time); return $add_end_time; } if (Cache::get($cache_key)) { $erci_xlh_num = Cache::get($cache_key); $add_end_time = $erci_xlh_num - $ext['locking_time']['next_time'] * 60; Cache::set($cache_key, $add_end_time, $add_end_time); } else { $add_end_time = ($ext['locking_time']['tow_no_locking_time'] - $ext['locking_time']['next_time']) * 60; } if ($add_end_time <= 30) { Cache::set($cache_key, 30, 30); return 30; } return $add_end_time; } /** * 巡乐会抽奖-重新加载奖池 */ private function reloadGiftPool($room_id, $gift_bag_id) { return db::name("vs_gift_bag_detail") ->field('id,quantity,remaining_number,weight,foreign_id,gift_bag_id') ->where(['gift_bag_id'=>$gift_bag_id]) ->select(); } /** * 巡乐会抽奖-获取礼物价格 */ private function getGiftPrice($gift_id) { static $gift_prices = []; if (!isset($gift_prices[$gift_id])) { $gift_prices[$gift_id] = db::name("vs_gift") ->where(['gid' => $gift_id]) ->value('gift_price'); } return $gift_prices[$gift_id]; } /** * 巡乐会抽奖-处理中奖通知 */ private function handlePrizeNotification($user_id,$gift_id, $room_id, $pan_xlh_num, $end_time,$locking_gift_id) { $FromUserInfo = db::name('user')->field('nickname,avatar')->where(['id' => $user_id])->find(); $gift_info = db::name('vs_gift')->field('gift_name')->where(['gid' => $gift_id])->find(); $locking_gift = db::name('vs_gift')->field('gift_name')->where(['gid' => $locking_gift_id])->find(); $room_data = db::name('vs_room')->field('room_name,user_id')->where(['id' => $room_id])->find(); $room_user = db::name('user')->where('id',$room_data['user_id'])->field('nickname,avatar')->find(); //锁定礼物 // $text = [ // 'gift_num' => $pan_xlh_num, // 'FromUserInfo' => $FromUserInfo, // 'end_time' => $end_time, // 'text' => $FromUserInfo['nickname'] . ' 在 ' . ' 巡乐会活动中 获得' . $gift_info['gift_name'] . '礼物 x 1' // ]; // // model('Chat')->sendMsg(1057, $room_id, $text); //推送礼物横幅 $text = $FromUserInfo['nickname'] .' 巡乐会活动中锁定礼物'.$locking_gift['gift_name'].' x ' .$pan_xlh_num ; $push = new Push(0, $room_id); $text_list_new = [ 'text' => $text, 'room_id' => $room_id, 'from_type' => 103, 'gift_num' => $pan_xlh_num, 'FromUserInfo' => $FromUserInfo, 'end_time' => $end_time, 'room_user' => $room_user ]; $push->xunlehui($text_list_new); } /** * 巡乐会抽奖-构建返回结果 */ private function buildResult($drawn_gifts) { $result_list = []; $gift_info_cache = []; foreach ($drawn_gifts as $gift_id => $count) { if (!isset($gift_info_cache[$gift_id])) { $gift_info = db::name('vs_gift') ->where(['gid' => $gift_id]) ->find(); $gift_info_cache[$gift_id] = $gift_info; } else { $gift_info = $gift_info_cache[$gift_id]; } $result_list[] = [ 'gift_id' => $gift_id, 'gift_name' => $gift_info['gift_name'], 'base_image' => $gift_info['base_image'], 'gift_price' => $gift_info['gift_price'], 'count' => $count, ]; } // 按价格降序排序 usort($result_list, function($a, $b) { return $b['gift_price'] <=> $a['gift_price']; }); return ['code' => 1, 'msg' => '成功', 'data' => $result_list]; } //错误日志记录------------------------------------------------------- /** * 记录抽奖错误日志到Redis */ private function recordDrawErrorToRedis($expectedCount, $actualCount, $room_id, $user_id, $gift_bag_id, $num, $gift_user_ids, $precomputedResults) { // 检查Redis连接是否可用 if (!$this->redis) { Log::record('Redis连接不可用,无法记录错误日志', 'error'); return; } try { $errorData = [ 'timestamp' => time(), 'expected_count' => $expectedCount, 'actual_count' => $actualCount, 'room_id' => $room_id, 'user_id' => $user_id, 'gift_bag_id' => $gift_bag_id, 'num' => $num, 'gift_user_ids' => $gift_user_ids, 'precomputed_results_count' => count($precomputedResults), 'precomputed_results' => $precomputedResults, 'error_type' => 'draw_count_mismatch' ]; // 使用正确的Redis方法存储数据 $key = 'blind_box_draw_world_errors_' . date('Y-m-d-H-i-s'); $this->redis->setex($key, 86400 * 7, json_encode($errorData)); } catch (\Exception $e) { Log::record('Redis操作失败: ' . $e->getMessage(), 'error'); } } }