diff --git a/application/api/model/BlindBoxTurntableGiftDrawWorld.php b/application/api/model/BlindBoxTurntableGiftDrawWorld.php index c1c1c19..2d1dd5b 100644 --- a/application/api/model/BlindBoxTurntableGiftDrawWorld.php +++ b/application/api/model/BlindBoxTurntableGiftDrawWorld.php @@ -36,85 +36,96 @@ class BlindBoxTurntableGiftDrawWorld extends Model */ public function draw_gift($gift_bag_id, $user_id, $gift_user_ids, $num = 1, $room_id = 0, $heart_id = 0,$auction_id = 0) { - 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; - } - ['bag_data' => $bag_data, 'room' => $room, 'xlh_ext' => $xlh_ext] = $loadResult['data']; + // 最大重试次数 + $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; + } + ['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']; - $availableGiftss = $precomputeResult['data']['availableGifts']; - $currentXlhPeriodsNum = $precomputeResult['data']['current_xlh_periods_num']; - $xlhIsPiaoPing = $precomputeResult['data']['xlh_is_piao_ping']; - $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']; + // 3. 预计算抽奖结果 + $precomputeResult = $this->precomputeDrawResults( + $bag_data, + $gift_user_ids, + $num + ); + if ($precomputeResult['code'] !== 1) { + return $precomputeResult; + } + $precomputedResults = $precomputeResult['data']['results']; + $availableGiftss = $precomputeResult['data']['availableGifts']; + $currentXlhPeriodsNum = $precomputeResult['data']['current_xlh_periods_num']; + $xlhIsPiaoPing = $precomputeResult['data']['xlh_is_piao_ping']; + $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, - $xlhIsPiaoPing, - $currentXlhPeriodsNum, - $room - ); + // 5. 处理后续操作(非事务性操作) + $this->handlePostDrawOperations( + $precomputedResults, + $boxTurntableLog, + $room_id, + $xlh_ext, + $xlhIsPiaoPing, + $currentXlhPeriodsNum, + $room + ); - // 6. 构建并返回结果 - return $this->buildDrawResult($boxTurntableLog, $giftCounts); + // 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, - ]; - $this->redis->setex($key, 86400 * 7, $e->getMessage(). ' ' .json_encode($errorData)); - return ['code' => 0, 'msg' => "网络加载失败,请重试!", 'data' => null]; + } 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]; + } } - } /** * 验证抽奖参数 @@ -471,54 +482,66 @@ class BlindBoxTurntableGiftDrawWorld extends Model $gift_user_num = count(explode(',', $gift_user_ids)); //人数 $bagGiftPrice = $bag_data['gift_price'] * $num * $gift_user_num; - db::startTrans(); - try { - // 1. 创建抽奖记录 - $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() - ]); + // 增加重试机制 + $maxRetries = 3; + for ($retry = 0; $retry < $maxRetries; $retry++) { + try { + db::startTrans(); + // 按照固定顺序处理事务步骤 + // 1. 扣除用户金币(优先处理) + $this->deductUserCoins($user_id, $bagGiftPrice, $room_id); - if (!$boxTurntableLog) { - throw new \Exception('添加盲盒转盘记录失败'); + // 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]; } - - // 2. 批量更新库存 - $this->batchUpdateGiftInventory($availableGiftss, $room_id); - - // 3. 批量插入礼包发放记录 - $this->batchInsertGiftBagReceiveLog($user_id, $boxTurntableLog, $bag_data, $room_id, $precomputedResults); - - // 4. 扣除用户金币 - $this->deductUserCoins($user_id, $bagGiftPrice, $room_id); - - //发送礼物 - $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(); - - // 5. 统计礼物数量 - $giftCounts = $this->countGifts($precomputedResults); - - return [ - 'code' => 1, - 'msg' => '事务执行成功', - 'data' => [ - 'log_id' => $boxTurntableLog, - 'gift_counts' => $giftCounts - ] - ]; - } catch (\Exception $e) { - db::rollback(); - return ['code' => 0, 'msg' => $e->getMessage(), 'data' => null]; } + return ['code' => 0, 'msg' => '操作超时,请重试', 'data' => null]; } /** @@ -533,9 +556,14 @@ class BlindBoxTurntableGiftDrawWorld extends Model $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); + $ret = db::name("vs_gift_bag_detail")->where('id',$giftId) + ->lock(true) // 添加悲观锁 + ->setDec('remaining_number', $count); if (!$ret) { Log::record('巡乐会更新礼物剩余数量: ' . $room_id."【数据】".var_export($precomputedResults, true),"info"); throw new \Exception('更新礼物剩余数量失败'); @@ -578,6 +606,15 @@ class BlindBoxTurntableGiftDrawWorld extends Model */ private function deductUserCoins($user_id, $bagGiftPrice, $room_id) { + // 使用悲观锁查询用户钱包 + $userWallet = db::name('user_wallet') + ->where(['user_id' => $user_id]) + ->lock(true) + ->find(); + + if (!$userWallet || $userWallet['coin'] < $bagGiftPrice) { + throw new \Exception('用户金币不足'); + } $walletUpdate = model('GiveGift')->change_user_cion_or_earnings_log( $user_id, $bagGiftPrice,