Files
midi-php/application/api/model/BlindBoxTurntableGiftDrawWorld.php

1439 lines
52 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\api\model;
use app\common\controller\Push;
use think\Cache;
use think\Log;
use think\Model;
use think\Db;
use think\Session;
use Redis;
/*
* 盲盒转盘优化后方法
*
*/
class BlindBoxTurntableGiftDrawWorld extends Model
{
private $redis;
public function __construct()
{
parent::__construct();
try {
$this->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)
{
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'];
// 5. 处理后续操作(非事务性操作)
$this->handlePostDrawOperations(
$precomputedResults,
$boxTurntableLog,
$room_id,
$xlh_ext,
$xlhIsPiaoPing,
$currentXlhPeriodsNum,
$room
);
// 6. 构建并返回结果
return $this->buildDrawResult($boxTurntableLog, $giftCounts);
} catch (\Exception $e) {
$key = 'blind_box_draw_errors_' . date('Y-m-d-H-i-s');
$errorData = [
'error_message' => $e->getMessage(),
'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, json_encode($errorData));
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 || !$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);
$xlh_is_piao_ping = 0;
$current_xlh_periods_num = $this->getCachedXlhPeriodsNum("get");
// 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('重置奖池后仍无可用礼物');
}
}
// 3. 预加载礼物信息(减少后续查询)
$giftInfoMap = $this->preloadGiftInfo($availableGifts);
// 4. 处理奖池重置逻辑
$needGiftNum = count($toarray) * $num;
$remaining_available_gifts=[];
if ($totalRemaining - $needGiftNum <= 0) {
$remaining_available_gifts = $availableGifts;
$availableGifts = $this->resetPoolAndReload($bag_data['id']);
if (empty($availableGifts)) {
throw new \Exception('重置奖池后仍无可用礼物');
}
$totalDrawTimes = 0;
$num = abs($totalRemaining - $num);
}
// 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,
'xlh_is_piao_ping' => $xlh_is_piao_ping,
'availableGifts' => $precomputedResults['precomputedResultss'],
]
];
}
/**
* 使用Alias Method预计算抽奖结果O(1)复杂度)
*/
private function precomputeResultsWithAliasMethod(
$toarray,
$num,
$availableGifts,
$giftInfoMap,
&$totalDrawTimes,
&$periods,
&$currentXlhPeriodsNum,
$remaining_available_gifts
) {
//计算$remaining_available_gifts 里面 remaining_number 的累加
$remaining_num = 0;
foreach ($remaining_available_gifts as $key=>$value) {
$remaining_num += $value['remaining_number'];
}
if($remaining_num > $num){
$availableGifts = $remaining_available_gifts;
$remaining_available_gifts = [];
}
$precomputedResults = [];
$precomputedResultss = [];
$giftBagIdToGift = [];
// 构建Alias表
$aliasTable = $this->buildAliasTable($availableGifts);
$remaining_num = 0;
foreach ($toarray as $giftUserId) {
if (!empty($remaining_available_gifts)) {
foreach ($remaining_available_gifts as $key=>$value) {
$remaining_num += $value['remaining_number'];
$gift = $giftInfoMap[$value['foreign_id']] ?? null;
for ($j = 0; $j < $value['remaining_number']; $j++) {
$precomputedResults[] = [
'gift_user_id' => $giftUserId,
'gift_bag_detail' => $value,
'gift' => $gift,
'draw_times' => $totalDrawTimes,
'periods' => $periods,
'j' => $j,
];
$totalDrawTimes++;
$currentXlhPeriodsNum++;
}
unset($remaining_available_gifts[$key]);
}
$numm = $num;
}else{
$numm = $num+$remaining_num;
}
for ($i = 0; $i < $numm; $i++) {
// 使用Alias Method选择礼物O(1)复杂度)
$selectedGift = $this->selectGiftWithAliasMethod($aliasTable);
if (!$selectedGift) {
return [];
}
// 获取礼物信息从预加载的map中获取避免查询
$giftId = $selectedGift['foreign_id'];
$gift = $giftInfoMap[$giftId] ?? null;
// if (!$gift) {
// continue;
// }
$precomputedResults[] = [
'gift_user_id' => $giftUserId,
'gift_bag_detail' => $selectedGift,
'gift' => $gift,
'draw_times' => $totalDrawTimes,
'periods' => $periods,
'i' => $i,
];
$precomputedResultss[] = [
'gift_user_id' => $giftUserId,
'gift_bag_detail' => $selectedGift,
'gift' => $gift,
'draw_times' => $totalDrawTimes,
'periods' => $periods,
'i' => $i,
];
$totalDrawTimes++;
$currentXlhPeriodsNum++;
// 更新Alias表模拟库存减少
$this->updateAliasTable($aliasTable, $selectedGift['id']);
}
}
return ['precomputedResults' => $precomputedResults, 'precomputedResultss' => $precomputedResultss];
}
/**
* 构建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;
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()
]);
if (!$boxTurntableLog) {
throw new \Exception('添加盲盒转盘记录失败');
}
// 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];
}
}
/**
* 批量更新礼物库存
*/
private function batchUpdateGiftInventory($precomputedResults, $room_id)
{
// 按礼物ID分组统计需要减少的数量
$inventoryUpdates = [];
foreach ($precomputedResults as $result) {
$giftId = $result['gift_bag_detail']['id'];
$inventoryUpdates[$giftId] = ($inventoryUpdates[$giftId] ?? 0) + 1;
}
// 批量更新
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) {
$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)
{
$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,
$xlhIsPiaoPing,
$currentXlhPeriodsNum,
$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, $xlhIsPiaoPing, $currentXlhPeriodsNum,$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, $xlhIsPiaoPing, $currentXlhPeriodsNum,$room)
{
$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",$currentXlhPeriodsNum);
// 处理飘屏
if ($xlhIsPiaoPing == 1 || $xlhIsPiaoPing == 2) {
$this->handleXlhPiaoPing($room_id, $xlh_ext, $xlhIsPiaoPing,$room);
}
// 更新巡乐会状态并推送
$this->updateAndPushXlhStatus($room_id, $xlh_ext, $currentXlhPeriodsNum);
}
private function handleXlhPiaoPing($room_id, $xlh_ext, $xlhIsPiaoPing,$room){
if($xlhIsPiaoPing == 1){
// 即将开始推送飘屏
$text = $room['room_name']."的巡乐会即将开始...";
// 推送礼物横幅
$push = new Push(UID, $room_id);
$text_list_new = [
'text' => $text,
'room_id' => $room_id,
'from_type' => 1
];
$push->xunlehui($text_list_new);
}
if($xlhIsPiaoPing == 2){
// 正式开始推送飘屏
$text = $room['room_name']."已正式开启巡乐会游戏...";
// 推送礼物横幅
$push = new Push(UID, $room_id);
$text_list_new = [
'text' => $text,
'room_id' => $room_id,
'from_type' => 2
];
$push->xunlehui($text_list_new);
// 巡乐会正式开始
$this_xlh_periods = db::name('vs_room')->where('id',$room_id)->value('xlh_periods');
$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' => []];
}
db::name("vs_room")->where('id',$room_id)->setInc('xlh_periods');//修改巡乐会期数
}
}
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('room_id',$room_id)->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;
}
// 推送
$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 = $num;
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. 获取并缓存盲盒配置
$cacheKey = "xlh_config_{$gift_bag_id}";
$ext = $this->getCachedXlhConfig();
// 2. 检查用户金币和房间状态
$bag_gift_price = $ext['xlh_box_price'] * $num;
$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];
}
$room = db::name('vs_room')
->field('id,room_name,is_open_blind_box_turntable,xlh_periods')
->where(['id' => $room_id])
->find();
if (!$room || $room['is_open_blind_box_turntable'] != 1) {
return ['code' => 0, 'msg' => '该房间未开启盲盒转盘', 'data' => null];
}
// 3. 检查巡乐会状态
$pan_xlh = db::name('vs_room_pan_xlh')
->where(['room_id' => $room_id, 'periods' => $room['xlh_periods']])
->find();
if (empty($pan_xlh)) {
return ['code' => 0, 'msg' => '未开始', 'data' => null];
}
if ($pan_xlh['end_time'] <= time()) {
return ['code' => 0, 'msg' => '本轮已结束', 'data' => null];
}
if ($pan_xlh['send_time'] != 0) {
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])
->select();
if (empty($gift_bag_detail)) {
return ['code' => 0, 'msg' => '当前房间未配置抽奖礼物,请联系管理员', 'data' => []];
}
// 计算总数量和剩余数量
$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);
}
// 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 = []; // 存储所有抽奖结果
while ($total_processed < $num) {
$current_batch = min($batch_size, $num - $total_processed);
db::startTrans();
try {
// 批量扣除金币(只在第一次事务中处理)
if ($total_processed == 0) {
$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 = []; // 用于记录库存变化
$gift_details_map = []; // 礼物详情映射
$remaining_available_gifts = $available_gifts;
for ($i = 0; $i < $current_batch; $i++) {
// 从可用礼物中选择
$selected_gift = $this->selectGiftFromAvailable($available_gifts);
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) {
$this->reset_gift_pool($gift_bag_id,$remaining_available_gifts);
$available_gifts = $this->reloadGiftPool($room_id, $gift_bag_id);
if (empty($available_gifts)) {
throw new \Exception('重置奖池后仍无可用礼物');
}
}
}
// 批量更新库存
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'],
'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'],
'num' => $last_update['num'],
'locking_end_time' => $last_update['end_time'],
'createtime' => time()
]);
$is_zhong_jiang = 1;
}
db::commit();
$total_processed += $current_batch;
} catch (\Exception $e) {
db::rollback();
Log::record('巡乐会抽奖失败: ' . $e->getMessage(),"infos");
return ['code' => 0, 'msg' => "抽奖中,请稍等...", 'data' => null];
}
}
// 7. 批量处理结果记录
try {
db::startTrans();
if(count($drawn_gifts) > $num){
Log::record('巡乐会抽奖失败-数量超限: ' . count($drawn_gifts),"infos");
return ['code' => 0, 'msg' => "抽奖中,请稍等...", 'data' => null];
}
// 批量插入礼包发放记录
$gift_records = [];
$periods = $room_pan_data[0]['periods'] ?? 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();
return ['code' => 0, 'msg' => $e->getMessage(), 'data' => null];
}
// 8. 处理推送消息
if ($is_zhong_jiang == 1) {
$this->handlePrizeNotification($user_id,$gift_id, $room_id, $pan_xlh_num, $end_time, $room['room_name'],$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_' . $room_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, $room_name,$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();
//锁定礼物
$text = [
'gift_num' => $pan_xlh_num,
'FromUserInfo' => $FromUserInfo,
'end_time' => $end_time,
'text' => $FromUserInfo['nickname'] . ' 在 ' . $room_name . ' 房间巡乐会中 获得' . $gift_info['gift_name'] . '礼物 x 1'
];
model('Chat')->sendMsg(1057, $room_id, $text);
//推送礼物横幅
$text = $FromUserInfo['nickname'] . ' 在 ' . $room_name.' 房间巡乐会锁定礼物'.$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' => 1
];
$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');
}
}
}