Files
yusheng-php/application/api/model/BlindBoxTurntableGiftDrawWorldNew.php

1371 lines
55 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(1);
$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 );
$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
);
// 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)
{
$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);
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)) {
$aliasTableForRemaining = $this->buildAliasTable($remaining_available_gifts);
// 计算上期剩余礼物总数
foreach ($gift_user_ids as $giftUserId) {
// 为每个用户先分配上期剩余礼物
$userRemainingAllocation = floor($remainingGiftCount / $total_num);
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++) {
$selectedGift = $this->selectGiftWithAliasMethod($aliasTableForRemaining);
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,
];
$this->getCachedXlhPeriodsNum("set");//添加寻乐会条件次数
$pan_total_draw_times = $this->getCachedPanDrawTimes($gift_bag_id,"set"); // 总抽奖次数
}
}
}
$pan_total_draw_times = $this->getCachedPanDrawTimes($gift_bag_id,"clear");//总抽奖次数重置
}
// 再从新奖池中分配剩余所需礼物
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) {
$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,
];
$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
// 更新房间巡乐会次数
$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];
}
// 验证用户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')
->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_times".$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)
{
// 重置剩余数量
Db::name("vs_gift_bag_detail")
->where(['gift_bag_id' => $gift_bag_id])
->update(['remaining_number' => Db::raw('quantity')]);
// 清除缓存
Cache::rm("pan_gift_bag_detail".$gift_bag_id);
// 重新获取可用礼物
return $this->getAvailableGifts($gift_bag_id);
}
/**
* 使用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++;
}
// 如果重试次数用完仍未抽中有效礼物返回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)
{
Db::name("vs_gift_bag_detail")
->where(['id' => $gift_bag_detail_id])
->setDec('remaining_number', 1);
}
/**
* 更新缓存中的奖池信息
*/
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) {
$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);
// 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($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)
{
// 使用悲观锁查询用户钱包
$userWallet = db::name('user_wallet')
->where(['user_id' => $user_id])
->find();
if (!$userWallet || $userWallet['coin'] < $bagGiftPrice) {
throw new \Exception('用户金币不足');
}
$walletUpdate = model('GiveGift')->change_user_cion_or_earnings_log(
$user_id,
$bagGiftPrice,
$room_id,
1,
10,
'盲盒转盘抽奖消耗'
);
if (!$walletUpdate) {
throw new \Exception('扣除用户金币失败');
}
//更新用户等级
$userLevel = model('Level')->user_level_data_update(
$user_id,
$bagGiftPrice,
1,
$room_id
);
if (!$userLevel) {
throw new \Exception('用户等级更新失败');
}
}
/**
* 批量更新礼物库存
*/
private function 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) {
$giftBagDetail = Db::name("vs_gift_bag_detail")
->where('id', $giftId)
->find();
$giftBagDetailCached = $this->getCachedGiftBagDetailItem($gift_bag_id, $giftId);
if (!$giftBagDetail) {
throw new \Exception("礼物详情不存在ID: " . $giftId);
}
$upRemainingNumber = $giftBagDetail['remaining_number'] - $count;
if($upRemainingNumber!=$giftBagDetailCached['remaining_number']){
$this->redis->setex( 'blind_box_draw_errors_' . date('Y-m-d-H-i-s'), 86400 * 7, "有并发礼物数量不一致礼物ID: " . $giftId . '数据库数量: '.$upRemainingNumber. '缓存数量: '.$giftBagDetailCached['remaining_number']. ' ' .json_encode($giftBagDetail) . ' ' .json_encode($giftBagDetailCached));
$upRemainingNumber = $giftBagDetailCached['remaining_number'];
}
// 检查库存是否足够
if ($upRemainingNumber < 0) {
throw new \Exception("礼物库存不足ID: " . $giftId);
}
if($upRemainingNumber = $giftBagDetail['remaining_number']){
return;
}
$ret = db::name("vs_gift_bag_detail")->where('id',$giftId)->update([
'remaining_number' => $upRemainingNumber
]);
if (!$ret) {
throw new \Exception('更新礼物剩余数量失败');
}
}
}
/**
* 批量插入礼包发放记录
*/
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($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 countGifts($precomputedResults)
{
$giftCounts = [];
foreach ($precomputedResults as $result) {
$giftId = $result['gift_bag_detail']['foreign_id'];
if (!isset($giftCounts[$giftId])) {
$giftCounts[$giftId] = [
'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[$giftId]['count']++;
}
return $giftCounts;
}
/**
* 处理抽奖后的后续操作(非事务性)
*/
private function handlePostDrawOperations(
$precomputedResults,
$boxTurntableLog,
$room_id
) {
// 获取巡乐会配置(使用缓存)
$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);
}
}
/**
* 批量插入盲盒转盘结果记录
*/
private function batchInsertBlindBoxResults($precomputedResults, $boxTurntableLog)
{
// 统计每个用户每个礼物的数量
$giftUserCounts = $this->countGifts($precomputedResults);
// 批量插入
$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 handleXlhOperations($room_id, $xlh_ext)
{
$xlhIsPiaoPing = 0;
$xlhPeriodsNum = $this->getCachedXlhPeriodsNum("get");
if($xlhPeriodsNum == $xlh_ext['open_condition']['waiting_start_num']){
$xlhIsPiaoPing = 1;
}
if($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){
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)
{
$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')->where(['user_id' => $user_id])->find();
if (!$user_waller || $user_waller['coin'] < $bag_gift_price) {
return ['code' => 0, 'msg' => '用户金币不足', 'data' => null];
}
$wallet_update = model('GiveGift')->change_user_cion_or_earnings_log(
$user_id,
$bag_gift_price,
$room_id,
1,
10,
$ext['gift_bag_name'].'抽奖消耗'
);
if (!$wallet_update) {
throw new \Exception('扣除用户金币失败');
}
$user_level = model('Level')->user_level_data_update(
$user_id,
$bag_gift_price,
1,
$room_id
);
if (!$user_level) {
throw new \Exception('用户等级更新失败');
}
}
// 处理当前批次的抽奖
$inventory_updates = []; // 用于记录库存变化
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);
if (!$selected_gift) {
$gift_bag_detail = $this->resetPoolAndReload($gift_bag_id);
$selected_gift = $this->selectGiftFromAvailable($gift_bag_detail);
if(!$selected_gift){
throw new \Exception('预计算抽奖失败,重置后无可用礼物');
}
}
// 记录库存变化
$inventory_updates[$selected_gift['id']] = ($inventory_updates[$selected_gift['id']] ?? 0) + 1;
// 记录抽中结果
$gift_id = $selected_gift['foreign_id'];
$drawn_gifts[$gift_id] = ($drawn_gifts[$gift_id] ?? 0) + 1;
// 处理主奖品
if ($gift_id == $ext['locking_condition']['selected_gift_id']) {
$pan_xlh_num = 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++;
cache::set('pan_xlh_num_'.$gift_bag_id.'_'.$pan_xlh['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);
$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) {
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);
$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);
}else{
$is_zhong_jiang = 0;
}
$total_processed += $current_batch;
}
// 7. 批量处理结果记录
if(count($drawn_gifts) > $num){
$key = 'xlh_draw_gift_errors_' . date('Y-m-d-H-i-s');
$errorData = [
'user_id' => $user_id,
'gift_bag_id' => $gift_bag_id,
'room_id' => $room_id,
'num' => $num,
'drawn_gifts_num' => count($drawn_gifts)
];
$this->redis->setex($key, 86400 * 7, "巡乐会抽奖失败-数量超限". ' ' .json_encode($errorData));
return ['code' => 0, 'msg' => "抽奖中,请稍等...", 'data' => null];
}
// 批量插入礼包发放记录
$gift_records = [];
$periods = $this->getCachedXlhPeriods('get') ?? 0;
$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) {
$pan_xlh_num = cache::get('pan_xlh_num_'.$gift_bag_id.'_'.$pan_xlh['id']);
$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)
{
$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)
{
$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, 30, 30);
return 30;
}
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];
}
}