This commit is contained in:
2025-12-21 16:04:34 +08:00
parent 4f32f98ad2
commit f9f0d01948
6 changed files with 649 additions and 2 deletions

View File

@@ -0,0 +1,43 @@
<?php
namespace app\api\controller;
use app\common\controller\BaseCom;
use app\common\service\LotteryService;
use think\Db;
use think\Exception;
use think\Log;
class Lottery extends BaseCom
{
/**
* 中奖统计接口
* @return json
*/
public function stat()
{
try {
$where = [
'uid' => $this->request->param('uid/d', 0),
'prize_type' => $this->request->param('prize_type/d', 0),
'start_time' => $this->request->param('start_time/d', 0),
'end_time' => $this->request->param('end_time/d', time())
];
$service = new LotteryService();
$stat = $service->statWinner($where);
return json([
'code' => 0,
'msg' => '统计成功',
'data' => $stat
]);
} catch (Exception $e) {
Log::error('中奖统计失败:' . $e->getMessage());
return json([
'code' => 1,
'msg' => $e->getMessage(),
'data' => []
]);
}
}
}

View File

@@ -183,13 +183,17 @@ class Room extends BaseCom
redis_lock_exit($key_name);
$room_id = input('room_id', 0);
$gift_id = input('gift_id', 0);
$gift_num = input('gift_num', 0);
$gift_num = input('gift_num', 1);
$to_uid = input('to_uid', 0);//收礼人ID逗号隔开的字符串
$type = input('type', 1);//1金币购买 2送背包礼物
$pit_number = input('pit_number', 0);
$heart_id = input('heart_id', 0);
$reslut = model('Room')->room_gift($this->uid, $to_uid, $gift_id, $gift_num, $type, $room_id, $pit_number,$heart_id);
if($gift_id == 88){
$reslut = model('Lottery')->gift($this->uid, $to_uid, $gift_id, $room_id,$gift_num);
}else{
$reslut = model('Room')->room_gift($this->uid, $to_uid, $gift_id, $gift_num, $type, $room_id, $pit_number,$heart_id);
}
redis_unlock($key_name);
return V($reslut['code'], $reslut['msg'], $reslut['data']);
}

View File

@@ -148,6 +148,10 @@ class Chat extends Model
//签约房 邀请用户上签约麦位
// SignRoomInviteUser = 1094,
//爆币房推送信息
// BlindCoinRoom = 1100,

View File

@@ -0,0 +1,149 @@
<?php
namespace app\api\model;
use app\common\service\LotteryService;
use think\Db;
use think\Exception;
use think\Log;
use think\Model;
class Lottery extends Model
{
/**
* 送礼参与抽奖接口
* @return json
*/
public function gift($send_uid, $recv_uid,$gift_id, $room_id, $num)
{
if (ceil($num) != $num) {
return ['code' => 0, 'msg' => '打赏礼物数量必须为整数', 'data' => null];
}
$toarray = explode(',',$recv_uid);
if(in_array($send_uid,$toarray)){
return ['code' => 0, 'msg' => '收礼人不能包含自己', 'data' => null];
}
$gift_info = Db::name('vs_gift')->where(['gid'=>$gift_id])
->field('gid as gift_id,gift_name,gift_price,file_type,base_image,play_image,gift_type,label,is_public_server')->find();
//送给所有人的总价格
$all_gift_price = $gift_info['gift_price'] * $num * count($toarray);
//判断是否有足够的金币
$user_waller = db::name('user_wallet')->where(['user_id'=>$send_uid])->find();
if ($user_waller['coin'] < $all_gift_price) {
return ['code' => 0, 'msg' => '用户金币不足', 'data' => null];
}
$nums = $num * count($toarray);
$this->lottery($send_uid,$gift_info['gift_price'],$nums,$room_id,$gift_id);
//送礼 开启事务
Db::startTrans();
//扣除用户金币并记录日志
$wallet_update = model('GiveGift')->change_user_cion_or_earnings_log($send_uid,$all_gift_price,$room_id,1,10,'用户金币购买礼物');
if(!$wallet_update){
Db::rollback();
return ['code' => 0, 'msg' => '扣除用户金币失败', 'data' => null];
}
//用户财富等级更新
$user_level = model('Level')->user_level_data_update($send_uid,$all_gift_price,1,$room_id);
if(!$user_level){
Db::rollback();
return ['code' => 0, 'msg' => '用户等级更新失败', 'data' => null];
}
//获取送礼用户昵称
$FromUserInfo = db::name('user')->where('id',$send_uid)->field('id as user_id,nickname,avatar,sex')->find();
$FromUserInfo['icon'][0] = model('UserData')->user_wealth_icon($send_uid);//财富图标
$FromUserInfo['icon'][1] = model('UserData')->user_charm_icon($send_uid);//魅力图标
$FromUserInfo['chat_bubble'] = model('Decorate')->user_decorate_detail($send_uid,9);//聊天气泡
//送给一人礼物的总价格(扣除用户的数额)
$gift_price = $gift_info['gift_price'] * $num;
foreach ($toarray as $k => $to_id){
// 1. 记录礼物赠送
$giftRecord = [
'send_uid' => $send_uid,
'recv_uid' => $recv_uid,
'gift_id' => $gift_id,
'gift_gold' => $gift_price,
'recv_gold' => $gift_price /2 ,
'small_pool_add' => $gift_price /2 ,
'create_time' => time()
];
$giftId = Db::name('bb_lottery_gift_record')->insertGetId($giftRecord);
//收礼记录行为日志
$give_gift = model('GiveGift')->change_user_give_gift_log($send_uid,$gift_id,$gift_price,$num,$to_id,2,1,$room_id,0);
if(!$give_gift){
Db::rollback();
return ['code' => 0, 'msg' => '送礼失败', 'data' => null];
}
//计算收礼人得益
$receiver_earnings = model('GiveGift')->receiver_earnings($to_id,$gift_price,2);
//增加收益并记录日志
$receiver = $this -> change_user_cion_or_earnings_log($to_id,$receiver_earnings,$room_id,2,11,'收礼增加收益');
//用户魅力等级更新
$user_level = model('Level')->user_level_data_update($to_id,$gift_price,2,$room_id);
if(!$user_level){
Db::rollback();
return ['code' => 0, 'msg' => '用户等级更新失败', 'data' => null];
}
$ToUserInfo = Db::name('user')->where(['id' => $to_id])->field('id as user_id,nickname,avatar,sex')->find();
$ToUserInfo['icon'][0] = model('UserData')->user_wealth_icon($to_id);//财富图标
$ToUserInfo['icon'][1] = model('UserData')->user_charm_icon($to_id);//魅力图标
$ToUserInfo['charm'] = db::name('vs_room_user_charm')->where(['user_id' => $to_id,'room_id' => $room_id])->value('charm');//魅力
$text = $FromUserInfo['nickname'] . ' 送给 ' . $ToUserInfo['nickname'].' 礼物 ' .$gift_info['gift_name'].' x ' .$num;
$text = [
'FromUserInfo' => $FromUserInfo,
'ToUserInfo' => $ToUserInfo,
'GiftInfo' => $gift_info,
'gift_num' => $num,
'text' => $text
];
//聊天室推送系统消息
model('Chat')->sendMsg(1005,$room_id,$text);
}
Db::commit();
return ['code' => 1, 'msg' => '送礼成功', 'data' => null];
}
//抽奖
public function lottery($send_uid,$gift_price,$num,$room_id,$giftId)
{
try {
for($i=0;$i<$num;$i++){
$gift_gold = $gift_price;
$service = new LotteryService();
$reslut = $service->handleGift($send_uid, $gift_gold, $giftId);
if ($reslut['code'] == 1) {
$result = $reslut['data'];
//(未开奖时)
if ($result['is_small_prize'] == 0) {
//不做处理
} else {//开奖
// 大奖
if ($result['is_big_prize'] == 1) {
$tet['text'] = '爆币大奖';
$tet['type'] = 1;
} else { // 小奖
$tet['text'] = '爆币小奖';
$tet['type'] = 2;
}
$tet['user_id'] = $send_uid;
$tet['play_image'] = '';
model('api/Chat')->sendMsg(1100,$room_id,$tet);
}
}
}
return V(1, '送礼成功');
} catch (Exception $e) {
Log::error('抽奖处理失败:' . $e->getMessage());
return V(0, $e->getMessage());
}
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace app\common\library;
class LotteryGiftLua
{
// 获取Lua脚本
public static function getLotteryLuaScript()
{
return <<<LUA
-- 接收参数send_uid, recv_uid, gift_gold, small_trigger_times, big_threshold, small_round, big_round, big_total_gold
local send_uid = ARGV[1]
local recv_uid = ARGV[2]
local gift_gold = tonumber(ARGV[3])
local small_trigger_times = tonumber(ARGV[4])
local big_threshold = tonumber(ARGV[5])
local small_round = tonumber(ARGV[6]) -- 小奖池当前轮次
local big_round = tonumber(ARGV[7]) -- 大奖池当前轮次
local big_total_gold = tonumber(ARGV[8]) -- 大奖池当前轮次金额
-- 核心约束:强制小轮次 ≥ 大轮次
if small_round < big_round then
small_round = big_round
end
-- 1. 基础金额拆分
local recv_gold = gift_gold * 0.5
local small_pool_add = gift_gold * 0.5
-- 2. Redis键定义
local small_round_key = "lottery:small_pool:round"
local small_total_times_key = "lottery:small_pool:total_times"
local small_total_gold_key = "lottery:small_pool:total_gold"
local big_round_key = "lottery:big_pool:round"
local big_total_gold_key = "lottery:big_pool:total_gold"
-- 初始化轮次确保Redis与入参一致
redis.call('set', small_round_key, small_round)
redis.call('set', big_round_key, big_round)
-- 初始化大奖池金额
if big_total_gold == 0 or big_total_gold == nil then
big_total_gold = tonumber(redis.call('get', big_total_gold_key) or 0)
end
-- 3. 小奖池累计更新
local small_total_times = tonumber(redis.call('incr', small_total_times_key))
local small_total_gold = tonumber(redis.call('get', small_total_gold_key) or 0)
small_total_gold = math.floor((small_total_gold + small_pool_add) * 100) / 100
redis.call('set', small_total_gold_key, small_total_gold)
-- 4. 返回结果初始化(区分大小轮次)
local result = {
send_uid = send_uid,
recv_uid = recv_uid,
gift_gold = gift_gold,
recv_gold = recv_gold,
small_pool_add = small_pool_add,
small_total_times = small_total_times,
small_total_gold = small_total_gold,
is_small_prize = 0,
small_prize_amount = 0,
small_remain_amount = 0,
is_big_prize = 0,
big_prize_amount = 0,
big_release_amount = 0,
small_round = small_round, -- 小奖池轮次
big_round = big_round, -- 大奖池轮次
big_total_gold = big_total_gold,
-- 新增:记录要划入下一轮的小奖开奖金额
small_prize_to_big_next_round = 0
}
-- 5. 小奖池开奖判断(小轮次+1
if small_total_times >= small_trigger_times then
result.is_small_prize = 1
-- 小奖随机比例
local small_ratio = math.random(2, 99)
result.small_prize_amount = math.floor(small_total_gold * small_ratio / 100 * 100) / 100
-- 小奖剩余金额(划入大奖池当前轮次)
result.small_remain_amount = math.floor((small_total_gold - result.small_prize_amount) * 100) / 100
-- 重置小奖池,小轮次+1
redis.call('set', small_total_times_key, 0)
redis.call('set', small_total_gold_key, 0)
small_round = small_round + 1
redis.call('set', small_round_key, small_round)
result.small_round = small_round
-- 6. 小奖剩余划入大奖池当前轮次
big_total_gold = math.floor((big_total_gold + result.small_remain_amount) * 100) / 100
redis.call('set', big_total_gold_key, big_total_gold)
result.big_total_gold = big_total_gold
-- 7. 大奖池开奖判断(大轮次+1
if big_total_gold >= big_threshold then
result.is_big_prize = 1
-- 大奖比例权重
local weight_sum = 20 + 50 + 30
local random_weight = math.random(1, weight_sum)
local big_ratio = random_weight <= 20 and 60 or (random_weight <= 70 and 70 or 80)
-- 大奖金额
result.big_prize_amount = math.floor(big_total_gold * big_ratio / 100 * 100) / 100
result.big_release_amount = math.floor((big_total_gold - result.big_prize_amount) * 100) / 100
-- 原有逻辑:重置大奖池,大轮次+1
redis.call('set', big_total_gold_key, 0)
big_round = big_round + 1
redis.call('set', big_round_key, big_round)
-- 强制保证小轮次≥大轮次
if small_round < big_round then
small_round = big_round
redis.call('set', small_round_key, small_round)
result.small_round = small_round
end
result.big_round = big_round
result.big_total_gold = 0
-- ===================== 新增核心逻辑 =====================
-- 小奖开奖金额累加到大奖池下一轮次新的big_round
result.small_prize_to_big_next_round = result.small_prize_amount
-- 原子性更新大奖池下一轮次金额
local new_big_total_gold = math.floor(result.small_prize_amount * 100) / 100
redis.call('set', big_total_gold_key, new_big_total_gold)
result.big_total_gold = new_big_total_gold
-- ======================================================
end
end
-- 返回结果
return cjson.encode(result)
LUA;
}
}

View File

@@ -0,0 +1,312 @@
<?php
namespace app\common\service;
use app\common\library\LotteryGiftLua;
use think\Cache;
use think\Db;
use think\Exception;
class LotteryService
{
// Redis实例
private $redis;
// 配置参数
private $config;
public function __construct()
{
$this->redis = Cache::store('redis')->handler();
// 加载配置
$this->config = Db::name('bb_lottery_config')->column('value', 'key');
// 初始化Redis缓存若Redis数据丢失从数据库恢复
$this->initRedisFromDb();
}
/**
* 缓存恢复:独立恢复大小轮次+对应金额
*/
private function initRedisFromDb()
{
// 1. 恢复小奖池轮次取pool_type=1的最大times
$maxSmallRound = Db::name('lottery_pool_flow')->where('pool_type', 1)->max('times') ?: 1;
if (!$this->redis->get('lottery:small_pool:round')) {
$this->redis->set('lottery:small_pool:round', $maxSmallRound);
}
// 2. 恢复大奖池轮次取pool_type=2的最大times
$maxBigRound = Db::name('lottery_pool_flow')->where('pool_type', 2)->max('times') ?: 1;
if (!$this->redis->get('lottery:big_pool:round')) {
$this->redis->set('lottery:big_pool:round', $maxBigRound);
}
// 3. 恢复小奖池当前轮次的次数/金额
$small_round = intval($this->redis->get('lottery:small_pool:round'));
if (!$this->redis->get('lottery:small_pool:total_times')) {
$smallTotalTimes = Db::name('lottery_pool_flow')
->where(['pool_type' => 1, 'type' => 1, 'times' => $small_round])
->count();
$this->redis->set('lottery:small_pool:total_times', $smallTotalTimes);
}
if (!$this->redis->get('lottery:small_pool:total_gold')) {
$smallTotalGold = Db::name('lottery_pool_flow')
->where(['pool_type' => 1, 'type' => 1, 'times' => $small_round])
->sum('amount') ?: 0;
$this->redis->set('lottery:small_pool:total_gold', $smallTotalGold);
}
// 4. 恢复大奖池当前轮次的金额
$big_round = intval($this->redis->get('lottery:big_pool:round'));
if (!$this->redis->get('lottery:big_pool:total_gold')) {
$bigAddGold = Db::name('lottery_pool_flow')
->where(['pool_type' => 2, 'type' => 3, 'times' => $big_round])
->sum('amount') ?: 0;
$bigReduceGold = Db::name('lottery_pool_flow')
->where(['pool_type' => 2, 'type' => [2,4], 'times' => $big_round])
->sum('amount') ?: 0;
$this->redis->set('lottery:big_pool:total_gold', $bigAddGold + $bigReduceGold);
}
}
/**
* 处理送礼抽奖逻辑
* @param int $send_uid 送礼用户ID
* @param float $gift_gold 礼物金币数
* @param int $giftId 礼物ID
* @return array 处理结果
* @throws Exception
*/
public function handleGift($send_uid, $gift_gold, $giftId)
{
// 参数校验
if ($gift_gold <= 0 || !$send_uid) {
throw new Exception('参数错误');
}
// 读取配置+独立轮次+大奖池金额
$small_trigger_times = intval($this->config['small_pool_trigger_times'] ?? 200);
$big_threshold = floatval($this->config['big_pool_threshold'] ?? 1000);
$small_round = intval($this->redis->get('lottery:small_pool:round') ?: 1);
$big_round = intval($this->redis->get('lottery:big_pool:round') ?: 1);
$big_total_gold = floatval($this->redis->get('lottery:big_pool:total_gold') ?: 0);
// 加载Lua脚本
$luaSha = LotteryGiftLua::getLotteryLuaScript();
// 执行Lua脚本入参small_round + big_round
$result = $this->redis->evalSha($luaSha, [
$send_uid, 0, $gift_gold,
$small_trigger_times, $big_threshold,
$small_round, $big_round, $big_total_gold
], 0);
$result = json_decode($result, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Lua脚本执行失败');
}
// 开启数据库事务
Db::startTrans();
try {
// . 1记录小奖池累计流水未开奖时
if ($result['is_small_prize'] == 0) {
$this->addPoolFlow(
1, // 小奖池
1, // 累计
$result['small_pool_add'],
$result['small_total_gold'] - $result['small_pool_add'],
$result['small_total_gold'],
$giftId,
$result['small_round'], // 新增:传入轮次
"小奖池累计:用户{$send_uid}送礼,轮次{$result['current_round']}"
);
} else {
$winnerUid = $send_uid; // 奖默认给当前送礼用户
// 1. 只要开小奖,小奖剩余划入大奖池流水
$this->addPoolFlow(
2, // 大奖池
3, // 划转
$result['small_remain_amount'],//小奖剩余金额
$result['big_total_gold'] - $result['small_remain_amount'],
$result['big_total_gold'],
$giftId,
$result['big_round'] - ($result['is_big_prize'] ? 1 : 0),
"小奖剩余划转大奖池:{$result['small_remain_amount']}金币"
);
//2.开小奖剩余划入大奖后 大奖够开奖
if ($result['is_big_prize'] == 1) {
// 大奖中奖记录
$this->addWinnerRecord(
$winnerUid,
2, // 大奖
$result['big_prize_amount'],//中奖金额
$result['big_total_gold'], // 开奖时大奖池金额
$this->getBigRatio($result['big_prize_amount'], $result['big_total_gold']),
$result['big_release_amount']//释放金额
);
// 大奖释放流水
$this->addPoolFlow(
2, // 大奖池
4, // 释放
-$result['big_release_amount'],//释放金额
$result['big_total_gold'],// 开奖时大奖池金额
0,
$giftId,
$result['big_round'] - 1, // 关联已结束的小奖池轮次
"大奖释放金额:{$result['big_release_amount']}金币"
);
// 4. 小奖开奖金额划转下一次大奖池流水
if ($result['small_prize_to_big_next_round'] > 0) {
$this->addPoolFlow(
2,// 大奖池
3,// 划转
$result['small_prize_to_big_next_round'],
0,
$result['small_prize_to_big_next_round'],
$giftId,
$result['big_round'],
"小奖开奖金额划入大奖池下一轮:大轮次{$result['big_round']},金额{$result['small_prize_to_big_next_round']}");
}
} else {//只有小奖中奖
// 小奖中奖记录
$this->addWinnerRecord(
$winnerUid,
1, // 小奖
$result['small_prize_amount'],//中奖金额
$result['small_total_gold'],//奖池总金额
$this->getSmallRatio($result['small_prize_amount'], $result['small_total_gold']),//中奖比例
0 //释放金额
);
// 3. 小奖池开奖流水
$this->addPoolFlow(
1, // 小奖池
2, // 开奖扣除
-$result['small_total_gold'],
$result['small_total_gold'],
0,
$giftId,
$result['small_round'] - 1, // 开奖轮次为当前轮次-1已结束的轮次
"小奖池开奖:轮次" . ($result['small_round'] - 1).",累计{$result['small_total_gold']}金币"
);
}
}
Db::commit();
return [
'code' => 1,
'msg' => '处理成功',
'data' => $result
];
} catch (Exception $e) {
Db::rollback();
throw new Exception($e->getMessage());
}
}
/**
* 添加奖池流水
* @param int $pool_type 奖池类型1-小 2-大
* @param int $type 流水类型1-累计 2-开奖 3-划转 4-释放
* @param float $amount 金额
* @param float $before_amount 操作前金额
* @param float $after_amount 操作后金额
* @param int $relate_id 关联ID
* @param int $times 轮次
* @param string $remark 备注
*/
private function addPoolFlow($pool_type, $type, $amount, $before_amount, $after_amount, $relate_id, $times, $remark)
{
Db::name('bb_lottery_pool_flow')->insert([
'pool_type' => $pool_type,
'type' => $type,
'amount' => $amount,
'before_amount' => $before_amount,
'after_amount' => $after_amount,
'relate_id' => $relate_id,
'times' => $times, // 新增:写入轮次
'remark' => $remark,
'create_time' => time()
]);
}
/**
* 添加中奖记录
* @param int $uid 中奖用户ID
* @param int $prize_type 奖项类型1-小 2-大
* @param float $prize_amount 中奖金额
* @param float $pool_amount 奖池总金额
* @param int $ratio 中奖比例
* @param float $release_amount 释放金额
*/
private function addWinnerRecord($uid, $prize_type, $prize_amount, $pool_amount, $ratio, $release_amount)
{
Db::name('bb_lottery_winner_record')->insert([
'uid' => $uid,
'prize_type' => $prize_type,
'prize_amount' => $prize_amount,
'pool_amount' => $pool_amount,
'ratio' => $ratio,
'release_amount' => $release_amount,
'create_time' => time(),
'status' => 1 // 已发放
]);
// 此处可添加用户金币入账逻辑(如更新用户金币表)
}
/**
* 计算小奖中奖比例
* @param float $prize_amount 中奖金额
* @param float $pool_amount 奖池金额
* @return int 比例(%
*/
private function getSmallRatio($prize_amount, $pool_amount)
{
return intval(round($prize_amount / $pool_amount * 100));
}
/**
* 计算大奖中奖比例
* @param float $prize_amount 中奖金额
* @param float $pool_amount 奖池金额
* @return int 比例(%
*/
private function getBigRatio($prize_amount, $pool_amount)
{
return intval(round($prize_amount / $pool_amount * 100));
}
/**
* 统计中奖数据
* @param array $where 筛选条件如uid、prize_type、time
* @return array 统计结果
*/
public function statWinner($where = [])
{
$query = Db::name('bb_lottery_winner_record');
if (!empty($where['uid'])) {
$query->where('uid', $where['uid']);
}
if (!empty($where['prize_type'])) {
$query->where('prize_type', $where['prize_type']);
}
if (!empty($where['start_time']) && !empty($where['end_time'])) {
$query->whereBetween('create_time', [$where['start_time'], $where['end_time']]);
}
// 总中奖金额、总释放金额、中奖次数
$stat = $query->field([
'SUM(prize_amount) as total_prize',
'SUM(release_amount) as total_release',
'COUNT(id) as total_times'
])->find();
return [
'total_prize' => $stat['total_prize'] ?? 0,
'total_release' => $stat['total_release'] ?? 0,
'total_times' => $stat['total_times'] ?? 0
];
}
}