Files
yusheng-php/application/api/model/BlindBoxTurntableGiftDrawWorldNew.php
2026-01-17 16:09:14 +08:00

1901 lines
79 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 BlindBoxTurntableGiftDrawWorldNew 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(10);
$this->cache_time = 60*60; //缓存一小时
} 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 {
// 收礼人
$gift_user_ids = explode(',', $gift_user_ids);
$total_num = $num * count($gift_user_ids); //总数量
$bag_data = $this->getCachedGiftBag($gift_bag_id); //获取转盘信息
$total_price = $bag_data['gift_price'] * $total_num; //礼包支付总价格
$periods = $bag_data['periods']; //期数
//1. 验证参数并提前处理错误
$validationResult = $this->validateDrawParameters($gift_bag_id, $user_id, $gift_user_ids,$total_price);
if ($validationResult !== true) {
return $validationResult;
}
//2.预计算抽奖结果
$precomputeResult = $this->precomputeDrawResults($gift_bag_id, $total_num, $gift_user_ids,$periods,$user_id );
$precomputedResults = $precomputeResult['precomputedResults']; //预计算结果集
$availableGiftss = $precomputeResult['precomputedResultss']; //可用礼物/需更新的礼物/需更新
if (count($precomputedResults) != $total_num) {
// 记录错误到Redis
// 使用正确的Redis方法存储数据
$key = 'blind_box_draw_world_errors_' . date('Y-m-d-H-i-s');
$errorData = [
'total_num' => $total_num,
'actual_num' => count($precomputedResults),
'room_id' => $room_id,
'user_id' => $user_id,
'gift_bag_id' => $gift_bag_id,
'num' => $num,
'gift_user_ids' => $gift_user_ids,
'precomputedResults' => $precomputedResults,
];
$this->redis->setex($key, 86400 * 7, "超出数量".json_encode($errorData));
return ['code' => 0, 'msg' => '活动火爆,请稍后重试!', 'data' => null];
}
$giftCounts = $this->countGifts($precomputedResults);
foreach ($giftCounts as $giftId => $count) {
if($count['count'] > $count['quantity']){
$key = 'blind_box_draw_world_errors_' . date('Y-m-d-H-i-s');
$errorData = [
'count' => $count['count'],
'quantity' => $count['quantity'],
'gift_bag_id' => $gift_bag_id,
'user_id' => $user_id,
'gift_user_ids' => $gift_user_ids,
'num' => $num,
'giftUserCountsJianCha' => $count,
];
$this->redis->setex($key, 86400 * 7, "礼物数量超出限制 ".json_encode($errorData));
return ['code' => 0, 'msg' => '活动火爆,请稍后重试!', 'data' => null];
}
}
// 4. 执行抽奖事务(核心操作)
$transactionResult = $this->executeDrawTransaction(
$bag_data,
$user_id,
$room_id,
$num,
$total_price,
$precomputedResults,
$availableGiftss,
$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,
$total_num
);
// 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));
}
return ['code' => 0, 'msg' => "活动火爆,请稍后重试!", 'data' => null];
}
}
/**
* 预计算抽奖结果
*/
private function precomputeDrawResults($gift_bag_id, $total_num, $gift_user_ids,$periods,$user_id)
{
$pan_total_draw_times = $this->getCachedPanDrawTimes($gift_bag_id); //总抽奖次数
$pan_total_remaining = $this->getCachedPanTotalRemaining($gift_bag_id); //剩余数量
//获取可用礼物
$availableGifts = $this->getAvailableGifts($gift_bag_id,$pan_total_draw_times+$total_num);
$giftInfoMap = $this->getCachedPanGiftInfoMap($gift_bag_id);//预加载礼物信息
$remaining_available_gifts = [];
if (empty($availableGifts) ||$pan_total_remaining ==0) {
//重置奖池
$availableGifts = $this->resetPoolAndReload($gift_bag_id);
$periods = $this->getCachedGiftBag($gift_bag_id)['periods']??$periods; // 获取新期数
if (empty($availableGifts)) {
throw new \Exception('重置奖池后仍无可用礼物');
}
$pan_total_draw_times = $this->getCachedPanDrawTimes($gift_bag_id,"clear");//总抽奖次数重置
}else{
if ($pan_total_remaining < $total_num) {
$remaining_available_gifts = $availableGifts; // 保存当前剩余礼物作为上期剩余
//重置奖池
$availableGifts = $this->resetPoolAndReload($gift_bag_id);
if (empty($availableGifts)) {
throw new \Exception('重置奖池后仍无可用礼物');
}
$pan_total_draw_times = $this->getCachedPanDrawTimes($gift_bag_id,"clear");;//总抽奖次数重置
}
}
// 5. 使用Alias Method预计算抽奖结果O(1)复杂度)
$precomputedResults = [];
$precomputedResultss = [];
// 计算上期剩余礼物总数
$remainingGiftCount = array_sum(array_column($remaining_available_gifts, 'remaining_number'));
$newGiftsNeeded = max(0, $total_num - $remainingGiftCount); // 计算还需要多少礼物从新奖池中抽取
// 先从上期剩余礼物中分配
if (!empty($remaining_available_gifts)) {
// 计算上期剩余礼物总数
foreach ($gift_user_ids as $giftUserId) {
// 为每个用户先分配上期剩余礼物
$userRemainingAllocation = floor($remainingGiftCount / count($gift_user_ids));
if (count($gift_user_ids) > 0) { // 防止除零错误
$extraGifts = $remainingGiftCount % count($gift_user_ids);
if (array_search($giftUserId, $gift_user_ids) < $extraGifts) {
$userRemainingAllocation++;
}
}
for ($i = 0; $i < $userRemainingAllocation; $i++) {
if(!empty($remaining_available_gifts)){
$randomKey = array_rand($remaining_available_gifts);
$selectedGift = $remaining_available_gifts[$randomKey];
--$remaining_available_gifts[$randomKey]['remaining_number'];
if($remaining_available_gifts[$randomKey]['remaining_number'] <=0){
unset($remaining_available_gifts[$randomKey]);
}
if ($selectedGift) {
$gift = $giftInfoMap[$selectedGift['foreign_id']];
$precomputedResults[] = [
'gift_user_id' => $giftUserId,
'gift_bag_detail' => $selectedGift,
'gift' => $gift,
'draw_times' => $this->getCachedPanDrawTimes($gift_bag_id),
'periods' => $periods,
];
$xlh_ext = $this->getCachedGiftBag(13);
if(!empty($xlh_ext) && $xlh_ext['inlet_bag_id'] == $gift_bag_id) {
$this->getCachedXlhPeriodsNum("set");//添加寻乐会条件次数
}
$pan_total_draw_times = $this->getCachedPanDrawTimes($gift_bag_id,"set"); // 总抽奖次数
}
}
}
}
$pan_total_draw_times = $this->getCachedPanDrawTimes($gift_bag_id,"clear");//总抽奖次数重置
$periods = $this->getCachedGiftBag($gift_bag_id)['periods']??$periods; // 获取新期数
}
// 再从新奖池中分配剩余所需礼物
if ($newGiftsNeeded > 0 && !empty($availableGifts)) {
$aliasTableForNew = $this->buildAliasTable($availableGifts);
foreach ($gift_user_ids as $giftUserId) {
// 计算每个用户需要从新奖池获得的礼物数量
$userNewAllocation = floor($newGiftsNeeded / count($gift_user_ids));
if (count($gift_user_ids) > 0) {
$extraGifts = $newGiftsNeeded % count($gift_user_ids);
if (array_search($giftUserId, $gift_user_ids) < $extraGifts) {
$userNewAllocation++;
}
}
for ($i = 0; $i < $userNewAllocation; $i++) {
$selectedGift = $this->selectGiftWithAliasMethod($aliasTableForNew);
if(!$selectedGift){
//重置奖池
$availableGifts = $this->resetPoolAndReload($gift_bag_id);
$periods = $this->getCachedGiftBag($gift_bag_id)['periods']??$periods; // 获取新期数
if (empty($availableGifts)) {
throw new \Exception('重置奖池后仍无可用礼物');
}
$this->getCachedPanDrawTimes($gift_bag_id,"clear");//总抽奖次数重置
$aliasTableForNew = $this->buildAliasTable($availableGifts);
$selectedGift = $this->selectGiftWithAliasMethod($aliasTableForNew);
}
if ($selectedGift) {
$giftBagDetailCached = $this->getCachedGiftBagDetailItem($gift_bag_id, $selectedGift['id']);
if($giftBagDetailCached['remaining_number']<=0){
$aliasTableForNew = $this->buildAliasTable($availableGifts);
$selectedGift = $this->selectGiftWithAliasMethod($aliasTableForNew);
}
$gift = $giftInfoMap[$selectedGift['foreign_id']]??[];
//防止过高爆率,检查本期用户概率
//当前用户在本期的(补偿 防止漏洞 超过预期 把盘拉平)
//总抽奖金额(支出)
// $total_money = db::name('vs_gift_bag_receive_log')->where(['gift_bag_id' => $gift_bag_id,'user_id' => $user_id,'periods'=>$periods])->sum('bag_price');
// //总礼物价值(收入)
// $total_gift_money = db::name('vs_gift_bag_receive_log')->where(['gift_bag_id' => $gift_bag_id,'user_id' => $user_id,'periods'=>$periods])->sum('gift_price');
// if($total_money>0 && $total_gift_money>0){
// $ratio =round(($total_gift_money / $total_money),3) ?? 0;
// if($ratio > 1.03){
// $gift_bag_detail_duo = db::name("vs_gift_bag_detail")->where([ 'gift_bag_id' => $gift_bag_id,'remaining_number' => ['>',0]])->order('remaining_number', 'desc')->find();
// if($gift_bag_detail_duo){
// $gift = $giftInfoMap[$gift_bag_detail_duo['foreign_id']]??[];
// }
// }
// }
$precomputedResults[] = [
'gift_user_id' => $giftUserId,
'gift_bag_detail' => $selectedGift,
'gift' => $gift,
'draw_times' => $this->getCachedPanDrawTimes($gift_bag_id),
'periods' => $periods,
];
$precomputedResultss[] = [
'gift_user_id' => $giftUserId,
'gift_bag_detail' => $selectedGift,
'gift' => $gift,
'draw_times' => $this->getCachedPanDrawTimes($gift_bag_id),
'periods' => $periods,
];
//更新相关缓存数据
$pan_total_draw_times = $this->getCachedPanDrawTimes($gift_bag_id,"set"); //总抽奖次数+1
$pan_total_remaining = $this->getCachedPanTotalRemaining($gift_bag_id,"set"); //剩余数量-1
// 更新房间巡乐会次数
$xlh_ext = $this->getCachedGiftBag(13);
if(!empty($xlh_ext) && $xlh_ext['inlet_bag_id'] == $gift_bag_id) {
$this->getCachedXlhPeriodsNum("set");//添加寻乐会条件次数
}
// 更新Alias表
$this->updateAliasTable($aliasTableForNew, $selectedGift['id']);
$this->updateCachedGiftBagDetail($gift_bag_id, $selectedGift['id']); //更新缓存中礼物的数量
}
}
}
}
return ['precomputedResults' => $precomputedResults, 'precomputedResultss' => $precomputedResultss];
}
/**
* 更新Alias表模拟库存减少
*/
private function updateAliasTable(&$aliasTable, $giftId)
{
// 查找礼物在Alias表中的位置
$gifts = &$aliasTable['gifts'];
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 validateDrawParameters($gift_bag_id, $user_id, $gift_user_ids, $total_price)
{
// 提前验证收礼人
if (in_array($user_id, $gift_user_ids)) {
return ['code' => 0, 'msg' => "收礼人不能包含自己", 'data' => null];
}
foreach ($gift_user_ids as $gift_user_id){
//查询是否实名认证
$is_real = model('UserData')->real_name_info($gift_user_id);
if($is_real['code']==0){
return ['code' => 0, 'msg' => '收礼用户中有未实名,暂不支持收礼', 'data' => null];
}else{
if($is_real['data']['is_real'] !=1){
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];
}
// 检查用户金币
$user_waller = db::name('user_wallet_coin')
->where(['user_id' => $user_id])
->find();
if (!$user_waller) {
return ['code' => 0, 'msg' => '用户钱包不存在', 'data' => null];
}
if ($user_waller['coin'] < $total_price) {
return ['code' => 0, 'msg' => '用户金币不足', 'data' => null];
}
return true;
}
//获取转盘礼包详细信息【缓存】
private function getCachedGiftBag($gift_bag_id,$is_cache = true) {
$cacheKey = "pan_gift_bag".$gift_bag_id;
$gift_bag_data = Cache::get($cacheKey);
if (!$gift_bag_data || !$is_cache) {
$bag_data = db::name("vs_gift_bag")
->field('id,name,ext,periods')
->where('id', $gift_bag_id)
->find();
if (!$bag_data) {
return [];
}
$gift_bag_data = json_decode($bag_data['ext'], true);
$gift_bag_data['gift_bag_id'] = $gift_bag_id;
$gift_bag_data['gift_bag_name'] = $bag_data['name'];
$gift_bag_data['periods'] = $bag_data['periods'];
$gift_bag_data['gift_price'] = DB::name("vs_gift") ->where(['gid' => $gift_bag_data['gift_id']])->value('gift_price');
Cache::set($cacheKey, $gift_bag_data, $this->cache_time);
}
return $gift_bag_data;
}
//获取奖池详细信息【缓存】
private function getCachedGiftBagDetail($gift_bag_id,$is_cache = true) {
$cacheKey = "pan_gift_bag_detail".$gift_bag_id;
$pan_gift_bag_detail = Cache::get($cacheKey);
if (empty($pan_gift_bag_detail) || !$is_cache) {
$pan_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 (!$pan_gift_bag_detail) {
return [];
}
$total_quantity = array_sum(array_column( $pan_gift_bag_detail, 'quantity'));
$total_remaining = array_sum(array_column($pan_gift_bag_detail, 'remaining_number'));
$total_draw_times = max(0, $total_quantity - $total_remaining); //总抽奖次数
Cache::set($cacheKey, $pan_gift_bag_detail, $this->cache_time);
Cache::set("pan_total_quantity".$gift_bag_id, $total_quantity, $this->cache_time);
Cache::set("pan_total_remaining".$gift_bag_id, $total_remaining, $this->cache_time);
Cache::set("pan_total_draw".$gift_bag_id, $total_draw_times, $this->cache_time);
}
return $pan_gift_bag_detail;
}
/**
* 获取可用礼物
* @param $gift_bag_id
* @param $total_draw_times 总抽奖次数
*/
private function getAvailableGifts($gift_bag_id, $pan_total_draw_times = 0)
{
$gift_bag_detail_data = $this->getCachedGiftBagDetail($gift_bag_id);
$gift_detail_data = [];
foreach ($gift_bag_detail_data as $item) {
if($item['remaining_number'] > 0 && $item['weight']<=$pan_total_draw_times && $item['quantity'] > 0){
$gift_detail_data[] = $item;
}
}
return $gift_detail_data;
}
/**
* 重置奖池并重新加载
*/
private function resetPoolAndReload($gift_bag_id)
{
$lockKey = "reset_pool_lock_".$gift_bag_id;
// 尝试获取分布式锁
if ($this->redis->set($lockKey, 1, ['NX', 'EX' => 10])) {
try {
// 重置剩余数量
Db::name("vs_gift_bag_detail")
->where(['gift_bag_id' => $gift_bag_id])
->update(['remaining_number' => Db::raw('quantity')]);
//更新期数
db::name("vs_gift_bag")->where('id',$gift_bag_id)->setInc('periods');
// 清除缓存
Cache::rm("pan_gift_bag".$gift_bag_id);
Cache::rm("pan_gift_bag_detail".$gift_bag_id);
// 重新获取可用礼物
$getAvailableGifts = $this->getAvailableGifts($gift_bag_id);
if(array_sum(array_column($getAvailableGifts, 'remaining_number')) < array_sum(array_column($getAvailableGifts, 'quantity'))){
// 清除缓存
Cache::rm("pan_gift_bag".$gift_bag_id);
Cache::rm("pan_gift_bag_detail".$gift_bag_id);
throw new \Exception('系统繁忙,请稍后再试!');
}
return $getAvailableGifts;
} finally {
// 释放锁
$this->redis->del($lockKey);
}
} else {
// 如果无法获取锁,等待一段时间后重试或返回错误
throw new \Exception('系统繁忙,请稍后再试');
}
}
/**
* 使用Alias Method选择礼物O(1)复杂度)
*/
private function selectGiftWithAliasMethod($aliasTable)
{
if (!$aliasTable) return null;
// 添加重试机制最多尝试5次
$maxAttempts = 5;
$attempt = 0;
while ($attempt < $maxAttempts) {
$n = $aliasTable['n'];
$k = mt_rand(0, $n - 1);
// 随机选择
if (mt_rand() / mt_getrandmax() < $aliasTable['prob'][$k]) {
$selectedGift = $aliasTable['index_map'][$k];
} else {
$selectedGift = $aliasTable['index_map'][$aliasTable['alias'][$k]] ?? null;
}
// 检查选中的礼物是否还有库存
if ($selectedGift && $selectedGift['remaining_number'] > 0) {
return $selectedGift;
}
$attempt++;
}
// 兜底方案:遍历查找第一个有库存的礼物
foreach ($aliasTable['index_map'] as $gift) {
if ($gift['remaining_number'] > 0) {
return $gift;
}
}
// 如果重试次数用完仍未抽中有效礼物返回null
return null;
}
/**
* 构建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) {
if ($gift['remaining_number'] <= 0) continue;
$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
];
}
/**
* 更新礼物剩余数量
*/
private function updateGiftRemainingNumber($gift_bag_detail_id)
{
$result = Db::name("vs_gift_bag_detail")
->where(['id' => $gift_bag_detail_id])
->where('remaining_number', '>', 0) // 确保库存充足
->setDec('remaining_number', 1);
if (!$result) {
throw new \Exception('礼物库存不足');
}
}
/**
* 更新缓存中的奖池信息
*/
private function updateCachedGiftBagDetail($gift_bag_id, $detail_id, $decrement = 1) {
$cacheKey = "pan_gift_bag_detail".$gift_bag_id;
$gift_bag_detail_data = Cache::get($cacheKey);
if ($gift_bag_detail_data) {
foreach ($gift_bag_detail_data as &$item) {
if ($item['id'] == $detail_id) {
if( $item['remaining_number'] >= $decrement){
$item['remaining_number'] -= $decrement;
}
break;
}
}
Cache::set($cacheKey, $gift_bag_detail_data, $this->cache_time);
}
}
/**
* 获取缓存中的奖池单个礼物信息
*/
private function getCachedGiftBagDetailItem($gift_bag_id, $detail_id) {
$cacheKey = "pan_gift_bag_detail".$gift_bag_id;
$gift_bag_detail_item = Cache::get($cacheKey);
if($gift_bag_detail_item) {
foreach ($gift_bag_detail_item as $item) {
if ($item['id'] == $detail_id) {
return $item;
}
}
}
return [];
}
/**
* 获取奖池总库存
*/
private function getCachedPanTotalQuantity($gift_bag_id) {
$cacheKey = "pan_total_quantity".$gift_bag_id;
$pan_total_quantity = Cache::get($cacheKey);
if(!$pan_total_quantity) {
$pan_total_quantity = Db::name("vs_gift_bag_detail")
->where(['gift_bag_id' => $gift_bag_id])
->sum('quantity');
Cache::set($cacheKey, $pan_total_quantity, $this->cache_time);
}
return $pan_total_quantity;
}
/**
* 获取奖池剩余库存
*/
private function getCachedPanTotalRemaining($gift_bag_id, $type="get", $check_num=1) {
$cacheKey = "pan_total_remaining".$gift_bag_id;
$pan_total_quantity = Cache::get($cacheKey);
if($type == "set"){
$pan_total_quantity = $pan_total_quantity-$check_num;
if($pan_total_quantity < 0){
$pan_total_quantity = 0;
}
Cache::set($cacheKey, $pan_total_quantity, $this->cache_time);
}elseif($type == "clear"){
$pan_total_quantity = 0;
Cache::set($cacheKey, $pan_total_quantity, $this->cache_time);
} else{
if(!$pan_total_quantity) {
$pan_total_quantity = Db::name("vs_gift_bag_detail")
->where(['gift_bag_id' => $gift_bag_id])
->sum('remaining_number');
Cache::set($cacheKey, $pan_total_quantity, $this->cache_time);
}
}
return $pan_total_quantity;
}
/**
* 获取缓存的抽奖次数
*/
private function getCachedPanDrawTimes($gift_bag_id,$type="get") {
$cacheKey = "pan_total_draw".$gift_bag_id;
$total_draw_times = Cache::get($cacheKey) ?? 0;
if($type == "set"){
$total_draw_times = $total_draw_times+1;
Cache::set($cacheKey, $total_draw_times, $this->cache_time);
}elseif($type == "clear"){
$total_draw_times = 0;
Cache::set($cacheKey, $total_draw_times, $this->cache_time);
}
return $total_draw_times;
}
/**
* 获取缓存的巡乐会开启次数
*/
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 = "pan_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;
}
/**
* 获取奖池总库存
*/
private function getCachedPanGiftInfoMap($gift_bag_id) {
$cacheKey = "pan_gift_info_map".$gift_bag_id;
$gift_info_map = Cache::get($cacheKey);
if(!$gift_info_map) {
$gift_bag_detail = Db::name("vs_gift_bag_detail")
->field('foreign_id')
->where(['gift_bag_id' => $gift_bag_id])
->select();
$gift_info_map = [];
$gift_ids = array_unique(array_column($gift_bag_detail, 'foreign_id'));
if (!empty($gift_ids)) {
foreach ($gift_ids as $gift_id) {
$gift_info_map[$gift_id] = db::name("vs_gift")
->field('gid,gift_name,gift_price,base_image')
->where(['gid' => $gift_id])
->find();
}
}
Cache::set($cacheKey, $gift_info_map, $this->cache_time);
}
return $gift_info_map;
}
//开始更新操作:
/**
* 执行抽奖事务(核心操作)
*/
private function executeDrawTransaction($bag_data, $user_id, $room_id, $num,$total_price, $precomputedResults,$availableGiftss,$heart_id,$auction_id)
{
try {
db::startTrans();
// 按照固定顺序处理事务步骤
// 1. 扣除用户金币(优先处理)
$this->deductUserCoins($user_id, $total_price, $room_id,$bag_data);
// 2. 创建抽奖记录
$boxTurntableLog = db::name('vs_blind_box_turntable_log')->insertGetId([
'user_id' => $user_id,
'gift_bag_id' => $bag_data['gift_bag_id'],
'num' => $num,
'room_id' => $room_id,
'bag_price' => $bag_data['gift_price'],
'createtime' => time()
]);
if (!$boxTurntableLog) {
throw new \Exception('添加盲盒转盘记录失败');
}
// 3. 批量更新库存按ID排序避免死锁
$this->batchUpdateGiftInventory($bag_data['gift_bag_id'],$availableGiftss);
// 4. 批量插入礼包发放记录
$this->batchInsertGiftBagReceiveLog($bag_data,$user_id, $boxTurntableLog,$room_id, $precomputedResults);
// 5. 发送礼物
$result = $this->sendGiftsToRecipients($bag_data,$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();
throw new \Exception($e->getMessage());
}
}
/**
* 扣除用户金币
*/
private function deductUserCoins($user_id, $bagGiftPrice, $room_id , $bag_data=[])
{
// 使用悲观锁查询用户钱包
$userWallet = db::name('user_wallet_coin')
->where(['user_id' => $user_id])
->find();
if (!$userWallet || $userWallet['coin'] < $bagGiftPrice) {
throw new \Exception('用户金币不足');
}
$walletUpdate = model('api/UserWallet')->change_user_cion_log($user_id, $bagGiftPrice, $room_id, 10, $bag_data['gift_bag_name'] .'抽奖消耗');
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 batchUpdateGiftInventory($gift_bag_id,$precomputedResults)
{
// 按礼物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) {
$giftBagDetailCached = $this->getCachedGiftBagDetailItem($gift_bag_id, $giftId);
if(empty($giftBagDetailCached)){
$giftBagDetail = db::name("vs_gift_bag_detail")->where('id',$giftId)->find();
$upRemainingNumber = $giftBagDetail['remaining_number'] - $count;
}else{
$upRemainingNumber = $giftBagDetailCached['remaining_number'];
}
if($upRemainingNumber < 0){
throw new \Exception('礼物数量不足');
}
db::name("vs_gift_bag_detail")->where('id',$giftId)->update([
'remaining_number' => $upRemainingNumber
]);
}
}
/**
* 批量插入礼包发放记录
*/
private function batchInsertGiftBagReceiveLog($bag_data,$user_id, $boxTurntableLog,$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['gift_bag_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 sendGiftsToRecipients($bag_data,$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;
}
$res = model("api/SendGift")->single_send_gift_pan($user_id,$userGift['gift_user_id'],$userGift['gift_id'],$userGift['count'],1,$room_id,$bag_data['gift_bag_id'], $heart_id,$auction_id);
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 countGifts($precomputedResults,$key_type = 1)
{
$giftCounts = [];
foreach ($precomputedResults as $result) {
$giftId = $result['gift_bag_detail']['foreign_id'];
$gift_user_id = $result['gift_user_id'];
if($key_type ==1){
$key = $giftId;
}else{
$key = $giftId.'_'.$gift_user_id;
}
if (!isset($giftCounts[$key])) {
$giftCounts[$key] = [
'gift_user_id' => $result['gift_user_id'],
'gift_id' => $giftId,
'count' => 0,
'gift_price' => $result['gift']['gift_price'],
'quantity' => $result['gift_bag_detail']['quantity']
];
}
$giftCounts[$key]['count']++;
}
return $giftCounts;
}
/**
* 处理抽奖后的后续操作(非事务性)
*/
private function handlePostDrawOperations(
$precomputedResults,
$boxTurntableLog,
$room_id,
$total_num
) {
// 获取巡乐会配置(使用缓存)
$xlh_ext = $this->getCachedGiftBag(13);
// 1. 批量插入盲盒转盘结果记录
$this->batchInsertBlindBoxResults($precomputedResults, $boxTurntableLog);
// 2. 处理巡乐会相关操作
if (!empty($xlh_ext) && $xlh_ext['inlet_bag_id'] == $precomputedResults[0]['gift_bag_detail']['gift_bag_id']) {
$this->handleXlhOperations($room_id, $xlh_ext,$total_num);
}
}
/**
* 批量插入盲盒转盘结果记录
*/
private function batchInsertBlindBoxResults($precomputedResults, $boxTurntableLog)
{
// 统计每个用户每个礼物的数量
$giftUserCounts = $this->countGifts($precomputedResults,2);
// 批量插入
$batchInsertData = [];
foreach ($giftUserCounts as $userGift) {
$batchInsertData[] = [
'tid' => $boxTurntableLog,
'gift_user_id' => $userGift['gift_user_id'],
'gift_user_room_id' => db::name('vs_room_visitor')->where(['user_id'=>$userGift['gift_user_id']])->order('id desc')->value('room_id') ?? 0,
'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 handleXlhOperations($room_id, $xlh_ext,$total_num)
{
$xlhIsPiaoPing = 0;
$xlhPeriodsNum = $this->getCachedXlhPeriodsNum("get");
//上一次的数量
$xlhLastPeriodsNum = $xlhPeriodsNum-$total_num;
if($xlhLastPeriodsNum < $xlh_ext['open_condition']['waiting_start_num'] && $xlhPeriodsNum >= $xlh_ext['open_condition']['waiting_start_num']){
$xlhIsPiaoPing = 1;
}
if($xlhLastPeriodsNum < $xlh_ext['open_condition']['start_num'] && $xlhPeriodsNum >= $xlh_ext['open_condition']['start_num']){
$xlhIsPiaoPing = 2;
}
// 处理飘屏
if ($xlhIsPiaoPing == 1 || $xlhIsPiaoPing == 2) {
$this->handleXlhPiaoPing($room_id, $xlh_ext, $xlhIsPiaoPing);
}
$this->updateAndPushXlhStatus($room_id, $xlh_ext);
}
private function handleXlhPiaoPing($room_id, $xlh_ext, $xlhIsPiaoPing){
$pan_xlh_old = db::name('vs_room_pan_xlh')->where(['send_time' => 0])->order('id desc')->find(); //防止并发创建多个
if($pan_xlh_old){
return true;
}
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){
$xlh['waiting_start_num'] = $xlh_ext['open_condition']['waiting_start_num'];//等待开奖次数
$xlh['start_num'] = $xlh_ext['open_condition']['start_num'];//开始开奖次数
// 当前抽奖次数
$xlh['current_num'] = $this->getCachedXlhPeriodsNum("get");
$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);
}
/**
* 构建抽奖结果
*/
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
]
];
}
/*
* 巡乐会抽奖-----------------------------------------------------------------------------------------
*/
/*
* 巡乐会抽奖(优化版)
*/
public function xlh_draw_gift($user_id, $num, $room_id)
{
if(empty($room_id)){
$room_visitor = db::name('vs_room_visitor')->field('room_id')->where(['user_id' => $user_id])->order('id', 'desc')->find();
$room_id = $room_visitor['room_id'] ?? 0;
}
if(empty($room_id)){
return ['code' => 0, 'msg' => '用户不在房间,无法获取房主信息', 'data' => null];
}
$gift_bag_id = 13;
// 1. 获取并缓存盲盒配置
$ext = $this->getCachedGiftBag($gift_bag_id); //获取转盘信息
$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];
}
// 6. 预计算抽奖结果
$drawn_gifts = []; // 用于统计抽中结果
$main_prize_updates = []; // 用于记录主奖品更新
$is_zhong_jiang = 0;
// 批量处理配置
$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_coin')->where(['user_id' => $user_id])->find();
if (!$user_waller || $user_waller['coin'] < $bag_gift_price) {
return ['code' => 0, 'msg' => '用户金币不足', 'data' => null];
}
$wallet_update = model('api/UserWallet')->change_user_cion_log($user_id, $bag_gift_price, $room_id, 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 = []; // 用于记录库存变化
for ($i = 0; $i < $current_batch; $i++) {
$total_draw_times = $this->getCachedPanDrawTimes($gift_bag_id) ?? 0; //已抽奖次数
$availableGifts = $this->getAvailableGifts($gift_bag_id,$total_draw_times); //获取可用礼物
if (empty($availableGifts)) {
$availableGifts = $this->resetPoolAndReload($gift_bag_id);
if (empty($availableGifts)) {
throw new \Exception('重置奖池后仍无可用礼物');
}
}
// 从可用礼物中选择
$selected_gift = $this->selectGiftFromAvailable($availableGifts,$user_id);
if (!$selected_gift) {
$gift_bag_detail = $this->resetPoolAndReload($gift_bag_id);
$selected_gift = $this->selectGiftFromAvailable($gift_bag_detail,$user_id);
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 = cache::get('pan_xlh_num_'.$gift_bag_id.'_'.$pan_xlh['id']);
if(!$pan_xlh_num){
$pan_xlh_num = $pan_xlh['num'] ?? 0;
}
$pan_xlh_num++;
$pan_xlh_num_bf = $pan_xlh_num;
cache::set('pan_xlh_num_'.$gift_bag_id.'_'.$pan_xlh['id'], $pan_xlh_num,$this->cache_time);
$main_prize_updates[] = [
'num' => $pan_xlh_num,
'user_id' => $user_id,
'gift_id' => $gift_id
];
// 计算延长时间
$add_end_time = $this->calculateEndTime($pan_xlh_num, $ext);
$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
];
$this->updateCachedGiftBagDetail($gift_bag_id, $selected_gift['id']); // 更新缓存数据
$this->getCachedPanTotalRemaining($gift_bag_id,"set"); //剩余数量-1
$this->getCachedPanDrawTimes($gift_bag_id,"set"); //抽奖次数+1
// 检查是否需要重置奖池
$total_remaining = $this->getCachedPanTotalRemaining($gift_bag_id);
if ($total_remaining <= 0){// 剩余数量小于等于0且当前处理数量小于总数量
$availableGifts = $this->resetPoolAndReload($gift_bag_id);
if (empty($availableGifts)) {
throw new \Exception('重置奖池后仍无可用礼物');
}
}
}
// 批量更新库存
ksort($inventory_updates); // 按ID排序
foreach ($inventory_updates as $detail_id => $count) {
$result = db::name("vs_gift_bag_detail")
->where('id',$detail_id)
->where('remaining_number', '>=', $count) // 确保库存充足
->setDec('remaining_number', $count);
if (!$result) {
throw new \Exception('礼物库存不足');
}
}
// 处理主奖品更新
if (!empty($main_prize_updates)) {
$last_update = end($main_prize_updates);
$pan_xlh_num = cache::get('pan_xlh_num_'.$gift_bag_id.'_'.$pan_xlh['id']);
if($last_update['num'] >= $pan_xlh_num){
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()
]);
}
// 使用insertIgnore防止重复插入记录
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;
unset($main_prize_updates);
}
$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;
$giftInfoMap = $this->getCachedPanGiftInfoMap($gift_bag_id);
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' => $giftInfoMap[$gift_id]['gift_price'], //预加载礼物信息,
'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) {
if(cache::get('pan_xlh_num_'.$gift_bag_id.'_'.$pan_xlh['id'])){
$pan_xlh_num = cache::get('pan_xlh_num_'.$gift_bag_id.'_'.$pan_xlh['id']);
}else{
$pan_xlh_num = $pan_xlh_num_bf;
}
$this->handlePrizeNotification($user_id, $room_id, $pan_xlh_num, $pan_xlh['id'],$ext['locking_condition']['locking_gift_id']);
}
//删除缓存
cache::rm('pan_xlh_num_'.$gift_bag_id.'_'.$pan_xlh['id']);
// 9. 构建返回结果
return $this->buildResult($drawn_gifts);
}
/**
* 巡乐会抽奖-从可用礼物中选择一个
*/
private function selectGiftFromAvailable(array &$available_gifts, $user_id = 0)
{
// 添加针对特定用户和礼物的概率调整逻辑
$adjusted_gifts = $this->adjustGiftProbabilities($available_gifts, $user_id);
$remaining = array_sum(array_column($adjusted_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 ($adjusted_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;
}
/** [新加]
* 调整特定用户和礼物的概率
* @param array $gifts 可用礼物列表
* @param int $user_id 用户ID
* @return array 调整后的礼物列表
*/
private function adjustGiftProbabilities(array $gifts, $user_id = 0)
{
$ext = $this->getCachedGiftBag(13); //获取转盘信息
$selected_gift_id = $ext['locking_condition']['selected_gift_id'];
// 定义需要降低概率的用户和礼物组合
// 格式: [user_id => [gift_id => reduction_factor]]
// reduction_factor 是概率降低倍数,例如 0.5 表示概率减半
$probability_adjustments = [
// 示例用户ID为1001的用户抽中礼物ID为2001的概率降低为原来的50%
// 1001 => [2001 => 0.5],
//测试数据
// 20060 => [$selected_gift_id => 2],
// 在这里可以添加更多规则 【正式规则】
// 21222 => [$selected_gift_id => 6],//10012
// 21259 => [$selected_gift_id => 6],//10049
];
// 如果没有指定用户或该用户没有特殊概率调整,则直接返回原数组
if ($user_id <= 0 || !isset($probability_adjustments[$user_id])) {
return $gifts;
}
$adjusted_gifts = [];
foreach ($gifts as $gift) {
$adjusted_gift = $gift;
// 如果该礼物需要对当前用户降低概率
if (isset($probability_adjustments[$user_id][$gift['foreign_id']])) {
// 降低该礼物的剩余数量权重(相当于降低被抽中的概率)
$reduction_factor = $probability_adjustments[$user_id][$gift['foreign_id']];
// 使用更合理的概率降低方式
// reduction_factor为6表示只有原来的1/6概率即降低5/6概率
if (mt_rand(1, $reduction_factor) != 1) {
// 按概率移除此礼物将其剩余数量设为0
$adjusted_gift['remaining_number'] = 0;
}
}
$adjusted_gifts[] = $adjusted_gift;
}
return $adjusted_gifts;
}
/**
* 巡乐会抽奖-计算结束时间
*/
private function calculateEndTime($pan_xlh_num, $ext)
{
$cache_key = 'pna_xlh_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, 10, 10);
return 10;
}
return $add_end_time;
}
/**
* 巡乐会抽奖-处理中奖通知
*/
private function handlePrizeNotification($user_id, $room_id, $pan_xlh_num, $pan_xlh_id,$locking_gift_id)
{
$FromUserInfo = db::name('user')->field('nickname,avatar')->where(['id' => $user_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();
$end_time = db::name('vs_room_pan_xlh')->where('id',$pan_xlh_id)->value('end_time');
//锁定礼物
//推送礼物横幅
$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];
}
/** ***********2025-12-19*****************
* 盲盒转盘-11-12-直接落包抽奖
*/
public function draw_gift_drop_bag($gift_bag_id, $user_id, $num = 1, $room_id = 0)
{
try {
// 参数验证
$validation_result = $this->validateDrawParams($gift_bag_id, $user_id, $num);
if ($validation_result['code'] !== 1) {
return $validation_result;
}
// 获取转盘信息和用户钱包
$bag_data = $this->getCachedGiftBag($gift_bag_id);
$user_wallet = Db::name('user_wallet_coin')->where(['user_id' => $user_id])->find();
if (!$user_wallet) {
return ['code' => 0, 'msg' => '用户钱包不存在', 'data' => null];
}
$total_price = $bag_data['gift_price'] * $num;
if ($user_wallet['coin'] < $total_price) {
return ['code' => 0, 'msg' => '用户金币不足', 'data' => null];
}
// 检查奖池状态并准备礼物
$pool_status = $this->prepareGiftPool($gift_bag_id, $num);
if ($pool_status['code'] !== 1) {
throw new \Exception($pool_status['msg']);
}
$bag_data = $this->getCachedGiftBag($gift_bag_id);//防止重置奖池后数据有跟新-期数
$available_gifts = $pool_status['data']['available_gifts'];
$remaining_available_gifts = $pool_status['data']['remaining_gifts'] ?? [];
$periods = $bag_data['periods'];
// 开始事务处理
Db::startTrans();
try {
// 扣除用户金币
$this->deductUserCoins($user_id, $total_price, $room_id, $bag_data);
// 抽取礼物
$draw_result = $this->performGiftDrawing(
$gift_bag_id,
$available_gifts,
$remaining_available_gifts,
$num,
$periods
);
if ($draw_result['code'] !== 1) {
throw new \Exception($draw_result['msg']);
}
$precomputed_results = $draw_result['data']['precomputed_results'];
$drawn_gifts = $draw_result['data']['drawn_gifts'];
$available_giftss = $draw_result['data']['available_giftss'];
// 创建抽奖记录
$log_id = $this->createDrawLog($user_id, $gift_bag_id, $num, $room_id, $bag_data);
if (!$log_id) {
throw new \Exception('添加盲盒转盘记录失败');
}
// 批量更新库存
$this->batchUpdateGiftInventory($gift_bag_id, $available_giftss);
// 批量插入礼包发放记录
$this->batchInsertGiftBagReceiveLog($bag_data, $user_id, $log_id, $room_id, $precomputed_results);
// 发送礼物给用户
$send_result = $this->sendGiftsToRecipientsPack($drawn_gifts, $user_id, $bag_data);
if (isset($send_result['code']) && $send_result['code'] !== 1) {
throw new \Exception($send_result['msg']);
}
$gift_counts = $send_result['data'];
// 插入抽奖结果
$this->batchInsertBlindBoxResults($precomputed_results, $log_id);
//中大奖礼物飘屏
$this->pushGiftBanner($bag_data,$user_id,$gift_counts,$room_id);
Db::commit();
// 返回结果
return $this->buildDrawResult($log_id, $gift_counts);
} catch (\Exception $e) {
Db::rollback();
return ['code' => 0, 'msg' => $e->getMessage(), 'data' => null];
}
} catch (\Exception $e) {
$key = 'blind_box_draw_errors_' . date('Y-m-d-H-i-s');
$error_data = [
'gift_bag_id' => $gift_bag_id,
'user_id' => $user_id,
'num' => $num,
'room_id' => $room_id
];
if ($this->redis) {
$this->redis->setex($key, 86400 * 7, $e->getMessage() . ' ' . json_encode($error_data));
}
return ['code' => 0, 'msg' => "活动火爆,请稍后重试!", 'data' => null];
}
}
/**
* 验证抽奖参数
*/
private function validateDrawParams($gift_bag_id, $user_id, $num)
{
if (empty($user_id)) {
return ['code' => 0, 'msg' => '用户ID不能为空', 'data' => null];
}
if (empty($gift_bag_id)) {
return ['code' => 0, 'msg' => '盲盒转盘ID不能为空', 'data' => null];
}
if ($num <= 0) {
return ['code' => 0, 'msg' => '抽奖数量必须大于0', 'data' => null];
}
return ['code' => 1, 'msg' => '验证通过', 'data' => null];
}
/**
* 准备奖池
*/
private function prepareGiftPool($gift_bag_id, $num)
{
$pan_total_draw_times = $this->getCachedPanDrawTimes($gift_bag_id);
$pan_total_remaining = $this->getCachedPanTotalRemaining($gift_bag_id);
$available_gifts = $this->getAvailableGifts($gift_bag_id, $pan_total_draw_times + $num);
$remaining_gifts = [];
// 如果没有可用礼物或者剩余数量为0重置奖池
if (empty($available_gifts) || $pan_total_remaining == 0) {
$available_gifts = $this->resetPoolAndReload($gift_bag_id);
if (empty($available_gifts)) {
return ['code' => 0, 'msg' => '奖池无可用礼物', 'data' => null];
}
$this->getCachedPanDrawTimes($gift_bag_id, "clear");
} else if ($pan_total_remaining < $num) {
// 如果剩余数量不足,保存当前剩余礼物作为上期剩余
// 但只分配实际剩余数量的礼物,避免超额分配
$remaining_gifts = $available_gifts;
$available_gifts = $this->resetPoolAndReload($gift_bag_id);
if (empty($available_gifts)) {
return ['code' => 0, 'msg' => '奖池无可用礼物', 'data' => null];
}
$this->getCachedPanDrawTimes($gift_bag_id, "clear");
} else {
// 剩余数量足够,直接使用当前奖池
// 不需要特殊处理,正常流程即可
}
return [
'code' => 1,
'msg' => '奖池准备完成',
'data' => [
'available_gifts' => $available_gifts,
'remaining_gifts' => $remaining_gifts
]
];
}
/**
* 执行礼物抽取
*/
private function performGiftDrawing($gift_bag_id, $available_gifts, $remaining_gifts, $num, $periods)
{
$drawn_gifts = [];
$precomputed_results = [];
$available_giftss = [];
$inventory_updates = [];
$old_gift_num = 0;
// 先处理上期剩余的礼物,但要严格控制数量,避免超额分配
if (!empty($remaining_gifts)) {
// 获取上期实际剩余数量
$remaining_total = array_sum(array_column($remaining_gifts, 'remaining_number'));
// 计算实际可从剩余礼物中分配的数量
$actual_old_gift_num = min($num, $remaining_total);
$gift_queue = []; // 初始化礼物队列
foreach ($remaining_gifts as $remaining_gift) {
$gift_id = $remaining_gift['foreign_id'];//礼物ID
//查询上期这个礼物抽取数量----------------
$count_num = Db::name('vs_gift_bag_receive_pan_log')->where(['gift_bag_id' => $gift_bag_id,'gift_id' => $gift_id,'periods'=>$periods-1])->count();
$error_gifts[$gift_id] =[
//上期抽中数量
'count_num'=>$count_num,
'quantity'=>$remaining_gift['quantity'],
'remaining_number'=>$remaining_gift['remaining_number'] // 添加实际剩余数量
];
//上期这个礼物抽走数量与礼物上限数量相同剔除这个礼物
if ($count_num >= $remaining_gift['quantity']){
//错误日志记录--------
$key = 'blind_box_draw_shengyu1_errors_' . date('Y-m-d-H-i-s');
$errorData=[
'gift_bag_id' => $gift_bag_id,
'available_gifts' => $available_gifts,
'remaining_gifts' => $remaining_gifts,
'num' => $num,
'gift_id' => $gift_id,
'periods' => $periods,
];
$this->redis->setex($key, 86400 * 7, "礼物数量超出限制 ".json_encode($errorData));
//错误日志记录--------end
//剔除该礼物
continue;
}
// 将礼物按剩余数量添加到队列中
for ($i = 0; $i < $remaining_gift['remaining_number']; $i++) {
$gift_queue[] = $remaining_gift;
}
}
// 从礼物队列中分配礼物,直到达到实际需要的数量
for ($i = 0; $i < $actual_old_gift_num && $i < count($gift_queue); $i++) {
$selected_gift = $gift_queue[$i];
$gift_id = $selected_gift['foreign_id'];
//查询上把这个礼物抽取数量------防止并发多出容错----------
$drawn_gifts[$gift_id] = ($drawn_gifts[$gift_id] ?? 0) + 1;
$precomputed_results[] = [
'gift_id' => $gift_id,
'gift_detail_id' => $selected_gift['id'],
'gift_bag_detail' => $selected_gift,
'gift' => Db::name('vs_gift')->where(['gid' => $gift_id])->find(),
'gift_user_id' => 0,
'periods' => $periods,
];
$old_gift_num++;
}
//防止上把数据错误
foreach ($drawn_gifts as $gift_id => $count){
//正常数量
$normal_num = $error_gifts[$gift_id]['quantity'] - $error_gifts[$gift_id]['count_num'];
if($count > $normal_num){
//错误日志记录--------
$key = 'blind_box_draw_shengyu2_errors_' . date('Y-m-d-H-i-s');
$errorData=[
'gift_bag_id' => $gift_bag_id,
'available_gifts' => $available_gifts,
'remaining_gifts' => $remaining_gifts,
'num' => $num,
'gift_id' => $gift_id,
'periods' => $periods,
'error_gifts'=>$error_gifts,
'normal_num'=>$normal_num
];
$this->redis->setex($key, 86400 * 7, "礼物数量超出限制 ".json_encode($errorData));
//错误日志记录--------end
$drawn_gifts[$gift_id] = $normal_num;
}
}
}
// 计算还需要抽取的数量
$remaining_count = max(0, $num - $old_gift_num);
// 抽取新礼物
for ($i = 0; $i < $remaining_count; $i++) {
$selected_gift = $this->selectGiftFromAvailable($available_gifts);
if (!$selected_gift) {
$gift_bag_detail = $this->resetPoolAndReload($gift_bag_id);
$periods = $this->getCachedGiftBag($gift_bag_id)['periods']??$periods; // 获取新期数
$selected_gift = $this->selectGiftFromAvailable($gift_bag_detail);
if (!$selected_gift) {
return ['code' => 0, 'msg' => '预计算抽奖失败,重置后无可用礼物', 'data' => null];
}
}
// 记录库存变化
$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;
// 记录完整结果
$result_entry = [
'gift_id' => $gift_id,
'gift_detail_id' => $selected_gift['id'],
'gift_bag_detail' => $selected_gift,
'gift' => Db::name('vs_gift')->where(['gid' => $gift_id])->find(),
'gift_user_id' => 0,
'periods' => $periods,
];
$available_giftss[] = $result_entry;
$precomputed_results[] = $result_entry;
$this->updateCachedGiftBagDetail($gift_bag_id, $selected_gift['id']);
$this->getCachedPanTotalRemaining($gift_bag_id, "set");
$this->getCachedPanDrawTimes($gift_bag_id, "set");
}
// 检查是否需要重置奖池
$total_remaining = $this->getCachedPanTotalRemaining($gift_bag_id);
if ($total_remaining <= 0) {
$new_gifts = $this->resetPoolAndReload($gift_bag_id);
if (empty($new_gifts)) {
return ['code' => 0, 'msg' => '重置奖池后仍无可用礼物', 'data' => null];
}
}
return [
'code' => 1,
'msg' => '抽奖完成',
'data' => [
'drawn_gifts' => $drawn_gifts,
'precomputed_results' => $precomputed_results,
'available_giftss' => $available_giftss
]
];
}
/**
* 创建抽奖日志
*/
private function createDrawLog($user_id, $gift_bag_id, $num, $room_id, $bag_data)
{
return Db::name('vs_blind_box_turntable_log')->insertGetId([
'user_id' => $user_id,
'gift_bag_id' => $bag_data['gift_bag_id'],
'num' => $num,
'room_id' => $room_id,
'bag_price' => $bag_data['gift_price'],
'is_sued' => 1,
'createtime' => time()
]);
}
/**
* 发送礼物给接收者
*/
private function sendGiftsToRecipientsPack($drawn_gifts,$user_id,$bag_data)
{
$giftCounts =[];
// 批量发送礼物
foreach ($drawn_gifts as $gift_id => $count) {
$res = model('api/UserGiftPack')->change_user_gift_pack(
$user_id,
$gift_id,
$count,
model('api/UserGiftPack')::BLANK_BOX_DRAW_GIFT_GET,
$bag_data['gift_bag_name']."抽奖所得"
);
if ($res['code'] != 1) {
throw new \Exception($res['msg']);
}
$giftCounts[]=[
'gift_id' => $gift_id,
'count' => $count
];
}
return ['code' => 1, 'msg' => '发送礼物成功', 'data' => $giftCounts];
}
/**
* 岁月之城,时空之旅抽奖-处理中奖通知 推送礼物横幅
*/
private function pushGiftBanner($bag_data,$user_id,$gift_counts,$room_id)
{
if($gift_counts){
$user_nickname = Db::name('user')->where(['id'=>$user_id])->value('nickname');
$text_list_new = [];
foreach ($gift_counts as $value) {
$draw_gift = Db::name('vs_gift')->where(['gid'=>$value['gift_id']])->find();
if($draw_gift['is_public_server'] == 1){
$text_message = $user_nickname . '在' . $bag_data['gift_bag_name'] . '中获得了礼物' . $draw_gift['gift_name'] . 'X' . $value['count'];
$text_list_new[] = [
'text' => $text_message,
'gift_picture' => $draw_gift['base_image'],
'room_id' => $room_id,
'fromUserName' => $user_nickname,
'toUserName' => '',
'giftName' => $draw_gift['gift_name'],
'roomId' => $room_id,
'number' => $value['count'],
];
}
}
if(!empty($text_list_new)){
//推送礼物横幅
$push = new Push($user_id, $room_id);
$push->giftBanner($text_list_new);
}
}
}
}