红包
This commit is contained in:
104
application/api/controller/Redpacket.php
Normal file
104
application/api/controller/Redpacket.php
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\api\controller;
|
||||||
|
|
||||||
|
use app\common\controller\BaseCom;
|
||||||
|
use app\common\service\RedpacketService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包接口
|
||||||
|
*/
|
||||||
|
class Redpacket extends BaseCom
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发红包
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$data = $this->request->post();
|
||||||
|
|
||||||
|
$data['user_id'] = $this->uid;
|
||||||
|
|
||||||
|
$service = new RedpacketService();
|
||||||
|
$reslut = $service->create($data);
|
||||||
|
|
||||||
|
return V($reslut['code'], $reslut['msg'], $reslut['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抢红包
|
||||||
|
*/
|
||||||
|
public function grab()
|
||||||
|
{
|
||||||
|
$redpacketId = $this->request->post('redpacket_id');
|
||||||
|
$password = $this->request->post('password', '');
|
||||||
|
|
||||||
|
if (empty($redpacketId)) {
|
||||||
|
return V(0, '红包ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = new RedpacketService();
|
||||||
|
$reslut = $service->grabWithResult($redpacketId, $this->uid, $password);
|
||||||
|
|
||||||
|
return V($reslut['code'], $reslut['msg'], $reslut['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取抢红包结果
|
||||||
|
*/
|
||||||
|
public function grabResult()
|
||||||
|
{
|
||||||
|
$user = $this->auth->getUser();
|
||||||
|
$redpacketId = $this->request->get('redpacket_id');
|
||||||
|
|
||||||
|
if (empty($redpacketId)) {
|
||||||
|
$this->error('红包ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
$service = new RedpacketService();
|
||||||
|
$result = $service->getGrabResult($redpacketId, $user->id);
|
||||||
|
|
||||||
|
if (!$result) {
|
||||||
|
$this->error('红包不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success('获取成功', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 红包详情
|
||||||
|
*/
|
||||||
|
public function detail()
|
||||||
|
{
|
||||||
|
$redpacketId = $this->request->get('redpacket_id');
|
||||||
|
$currentUserId = $this->auth->isLogin() ? $this->auth->id : 0;
|
||||||
|
|
||||||
|
if (empty($redpacketId)) {
|
||||||
|
$this->error('红包ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$service = new RedpacketService();
|
||||||
|
$detail = $service->getDetail($redpacketId, $currentUserId);
|
||||||
|
|
||||||
|
if (!$detail) {
|
||||||
|
$this->error('红包不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success('获取成功', $detail);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取倒计时选项
|
||||||
|
*/
|
||||||
|
public function countdownOptions()
|
||||||
|
{
|
||||||
|
$options = \app\common\model\Redpacket::$countdownOptions;
|
||||||
|
$this->success('获取成功', $options);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
application/common/library/RedpacketLua.php
Normal file
105
application/common/library/RedpacketLua.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\library;
|
||||||
|
|
||||||
|
class RedpacketLua
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 抢红包Lua脚本
|
||||||
|
* 保证原子性操作,防止超抢
|
||||||
|
*/
|
||||||
|
public static function grabRedpacketScript()
|
||||||
|
{
|
||||||
|
return <<<LUA
|
||||||
|
-- KEYS[1]: 红包key, KEYS[2]: 用户集合key, KEYS[3]: 用户ID
|
||||||
|
-- ARGV[1]: 当前时间
|
||||||
|
|
||||||
|
local redpacketKey = KEYS[1]
|
||||||
|
local userSetKey = KEYS[2]
|
||||||
|
local userId = KEYS[3]
|
||||||
|
local currentTime = tonumber(ARGV[1])
|
||||||
|
|
||||||
|
-- 检查红包是否存在
|
||||||
|
local redpacketData = redis.call('HGETALL', redpacketKey)
|
||||||
|
if not redpacketData or #redpacketData == 0 then
|
||||||
|
return {0, "红包不存在"}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 将哈希数据转为table
|
||||||
|
local redpacket = {}
|
||||||
|
for i = 1, #redpacketData, 2 do
|
||||||
|
redpacket[redpacketData[i]] = redpacketData[i + 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 检查红包状态
|
||||||
|
local status = tonumber(redpacket['status'])
|
||||||
|
local startTime = tonumber(redpacket['start_time'])
|
||||||
|
local endTime = tonumber(redpacket['end_time'])
|
||||||
|
|
||||||
|
if status == 0 then
|
||||||
|
if currentTime < startTime then
|
||||||
|
return {0, "红包还未开始"}
|
||||||
|
else
|
||||||
|
-- 更新状态为进行中
|
||||||
|
redis.call('HSET', redpacketKey, 'status', 1)
|
||||||
|
status = 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if status ~= 1 then
|
||||||
|
return {0, "红包已结束"}
|
||||||
|
end
|
||||||
|
|
||||||
|
if currentTime > endTime then
|
||||||
|
redis.call('HSET', redpacketKey, 'status', 2)
|
||||||
|
return {0, "红包已结束"}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 检查是否已经抢过
|
||||||
|
local hasGrabbed = redis.call('SISMEMBER', userSetKey, userId)
|
||||||
|
if hasGrabbed == 1 then
|
||||||
|
return {0, "已经抢过该红包"}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 检查是否还有剩余
|
||||||
|
local leftAmount = tonumber(redpacket['left_amount'])
|
||||||
|
local leftCount = tonumber(redpacket['left_count'])
|
||||||
|
|
||||||
|
if leftCount <= 0 or leftAmount <= 0 then
|
||||||
|
return {0, "红包已抢完"}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 计算红包金额
|
||||||
|
local amount = 0
|
||||||
|
if leftCount == 1 then
|
||||||
|
-- 最后一个红包,获得剩余所有金额
|
||||||
|
amount = leftAmount
|
||||||
|
else
|
||||||
|
-- 随机算法:二倍均值法,保证公平性
|
||||||
|
local maxAmount = leftAmount / leftCount * 2
|
||||||
|
amount = math.random(1, math.floor(maxAmount * 100)) / 100
|
||||||
|
-- 确保金额不会超过剩余金额
|
||||||
|
if amount > leftAmount then
|
||||||
|
amount = leftAmount
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 更新红包数据
|
||||||
|
local newLeftAmount = leftAmount - amount
|
||||||
|
local newLeftCount = leftCount - 1
|
||||||
|
|
||||||
|
redis.call('HSET', redpacketKey, 'left_amount', newLeftAmount)
|
||||||
|
redis.call('HSET', redpacketKey, 'left_count', newLeftCount)
|
||||||
|
|
||||||
|
-- 标记用户已抢
|
||||||
|
redis.call('SADD', userSetKey, userId)
|
||||||
|
|
||||||
|
-- 如果抢完了,更新状态
|
||||||
|
if newLeftCount == 0 then
|
||||||
|
redis.call('HSET', redpacketKey, 'status', 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
return {1, tostring(amount)}
|
||||||
|
LUA;
|
||||||
|
}
|
||||||
|
}
|
||||||
130
application/common/model/Redpacket.php
Normal file
130
application/common/model/Redpacket.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
use think\Db;
|
||||||
|
|
||||||
|
class Redpacket extends Model
|
||||||
|
{
|
||||||
|
// 红包状态
|
||||||
|
const STATUS_PENDING = 0; // 未开始
|
||||||
|
const STATUS_ACTIVE = 1; // 进行中
|
||||||
|
const STATUS_FINISHED = 2; // 已结束
|
||||||
|
const STATUS_REFUNDED = 3; // 已退回
|
||||||
|
|
||||||
|
// 红包类型
|
||||||
|
const TYPE_NORMAL = 1; // 普通红包
|
||||||
|
const TYPE_PASSWORD = 2; // 口令红包
|
||||||
|
|
||||||
|
// 币种类型
|
||||||
|
const COIN_GOLD = 1; // 金币
|
||||||
|
const COIN_DIAMOND = 2; // 钻石
|
||||||
|
|
||||||
|
// 倒计时选项
|
||||||
|
public static $countdownOptions = [
|
||||||
|
0 => '立刻',
|
||||||
|
60 => '1分钟',
|
||||||
|
120 => '2分钟',
|
||||||
|
300 => '5分钟',
|
||||||
|
600 => '10分钟'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 领取条件
|
||||||
|
const CONDITION_NONE = 0;
|
||||||
|
const CONDITION_COLLECT_ROOM = 1;
|
||||||
|
const CONDITION_MIC_USER = 2;
|
||||||
|
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'createtime';
|
||||||
|
protected $updateTime = 'updatetime';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发红包
|
||||||
|
*/
|
||||||
|
public function createRedpacket($data)
|
||||||
|
{
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
// 验证用户余额
|
||||||
|
$wallet = Db::name('user_wallet')->where('user_id', $data['user_id'])->find();
|
||||||
|
|
||||||
|
$coinField = $data['coin_type'] == self::COIN_GOLD ? 'coin' : 'earnings';
|
||||||
|
if ($wallet[$coinField] < $data['total_amount']) {
|
||||||
|
return V(0, '余额不足');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扣除余额
|
||||||
|
Db::name('user_wallet')
|
||||||
|
->where('user_id', $data['user_id'])
|
||||||
|
->dec($coinField, $data['total_amount'])
|
||||||
|
->update();
|
||||||
|
|
||||||
|
// 计算开始时间
|
||||||
|
$startTime = $data['countdown'] > 0 ? (time() + $data['countdown']) : time();
|
||||||
|
$endTime = $startTime + 120; // 2分钟后结束
|
||||||
|
|
||||||
|
// 创建红包
|
||||||
|
$redpacketData = [
|
||||||
|
'user_id' => $data['user_id'],
|
||||||
|
'room_id' => $data['room_id'],
|
||||||
|
'type' => $data['type'],
|
||||||
|
'password' => $data['password'] ?? '',
|
||||||
|
'countdown' => $data['countdown'],
|
||||||
|
'coin_type' => $data['coin_type'],
|
||||||
|
'total_amount' => $data['total_amount'],
|
||||||
|
'total_count' => $data['total_count'],
|
||||||
|
'left_amount' => $data['total_amount'],
|
||||||
|
'left_count' => $data['total_count'],
|
||||||
|
'conditions' => $data['conditions'] ?? '',
|
||||||
|
'status' => $data['countdown'] > 0 ? self::STATUS_PENDING : self::STATUS_ACTIVE,
|
||||||
|
'start_time' => $startTime,
|
||||||
|
'end_time' => $endTime,
|
||||||
|
'createtime' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
$redpacketId = $this->insertGetId($redpacketData);
|
||||||
|
|
||||||
|
// 设置Redis缓存
|
||||||
|
$redis = \think\Cache::store('redis')->handler();
|
||||||
|
$redisKey = "redpacket:{$redpacketId}";
|
||||||
|
$redis->hMSet($redisKey, [
|
||||||
|
'total_amount' => $data['total_amount'],
|
||||||
|
'left_amount' => $data['total_amount'],
|
||||||
|
'total_count' => $data['total_count'],
|
||||||
|
'left_count' => $data['total_count'],
|
||||||
|
'status' => $redpacketData['status'],
|
||||||
|
'start_time' => $startTime,
|
||||||
|
'end_time' => $endTime
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 设置过期时间
|
||||||
|
$redis->expireAt($redisKey, $endTime + 3600); // 结束后保留1小时
|
||||||
|
|
||||||
|
Db::commit();
|
||||||
|
// return $redpacketId;
|
||||||
|
return V(1, '发红包成功', $redpacketId);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
return V(0, $e);
|
||||||
|
// throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取红包信息
|
||||||
|
*/
|
||||||
|
public function getRedpacketInfo($id)
|
||||||
|
{
|
||||||
|
$redpacket = $this->find($id);
|
||||||
|
if (!$redpacket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$redpacket = $redpacket->toArray();
|
||||||
|
$redpacket['nickname'] = Db::name('user')->where('id', $redpacket['user_id'])->value('nickname');
|
||||||
|
|
||||||
|
return $redpacket;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
application/common/model/RedpacketRecord.php
Normal file
12
application/common/model/RedpacketRecord.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
class RedpacketRecord extends Model
|
||||||
|
{
|
||||||
|
protected $autoWriteTimestamp = true;
|
||||||
|
protected $createTime = 'createtime';
|
||||||
|
protected $updateTime = false;
|
||||||
|
}
|
||||||
647
application/common/service/RedpacketService.php
Normal file
647
application/common/service/RedpacketService.php
Normal file
@@ -0,0 +1,647 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
use think\Db;
|
||||||
|
use think\Cache;
|
||||||
|
use app\common\model\Redpacket;
|
||||||
|
use app\common\model\RedpacketRecord;
|
||||||
|
use app\common\model\UserWallet;
|
||||||
|
use app\common\library\RedpacketLua;
|
||||||
|
|
||||||
|
class RedpacketService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 发红包
|
||||||
|
*/
|
||||||
|
public function create($data)
|
||||||
|
{
|
||||||
|
// 验证数据
|
||||||
|
$res = $this->validateCreateData($data);
|
||||||
|
if ($res['code'] == 0) {
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
$redpacketModel = new Redpacket();
|
||||||
|
return $redpacketModel->createRedpacket($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抢红包并返回详细结果
|
||||||
|
*/
|
||||||
|
public function grabWithResult($redpacketId, $userId, $password = '')
|
||||||
|
{
|
||||||
|
$redpacketModel = new Redpacket();
|
||||||
|
$redpacket = $redpacketModel->getRedpacketInfo($redpacketId);
|
||||||
|
|
||||||
|
if (!$redpacket) {
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => '红包不存在',
|
||||||
|
'data' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证口令红包
|
||||||
|
if ($redpacket['type'] == Redpacket::TYPE_PASSWORD) {
|
||||||
|
if (empty($password) || $password != $redpacket['password']) {
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => '口令错误',
|
||||||
|
'data' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证领取条件
|
||||||
|
$conditionCheck = $this->checkConditionsWithResult($redpacket, $userId);
|
||||||
|
if ($conditionCheck['code'] != 1) {
|
||||||
|
return $conditionCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查红包状态
|
||||||
|
$statusCheck = $this->checkRedpacketStatus($redpacket);
|
||||||
|
if ($statusCheck['code'] != 1) {
|
||||||
|
return $statusCheck;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经抢过
|
||||||
|
if ($this->hasUserGrabbed($redpacketId, $userId)) {
|
||||||
|
$detail = $this->getGrabResult($redpacketId, $userId);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => '已经抢过该红包',
|
||||||
|
'data' => $detail
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用Redis+Lua保证原子性操作
|
||||||
|
$redis = Cache::store('redis')->handler();
|
||||||
|
$script = RedpacketLua::grabRedpacketScript();
|
||||||
|
|
||||||
|
$redpacketKey = "redpacket:{$redpacketId}";
|
||||||
|
$userSetKey = "redpacket_users:{$redpacketId}";
|
||||||
|
|
||||||
|
$result = $redis->eval($script, [
|
||||||
|
$redpacketKey,
|
||||||
|
$userSetKey,
|
||||||
|
$userId,
|
||||||
|
time()
|
||||||
|
], 3);
|
||||||
|
|
||||||
|
if ($result[0] == 0) {
|
||||||
|
$message = $result[1];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => $message,
|
||||||
|
'data' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$amount = floatval($result[1]);
|
||||||
|
|
||||||
|
// Lua脚本执行成功,记录到数据库
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
// 创建领取记录
|
||||||
|
$recordData = [
|
||||||
|
'redpacket_id' => $redpacketId,
|
||||||
|
'user_id' => $userId,
|
||||||
|
'amount' => $amount
|
||||||
|
];
|
||||||
|
|
||||||
|
$recordModel = new RedpacketRecord();
|
||||||
|
$recordModel->save($recordData);
|
||||||
|
|
||||||
|
// 更新用户钱包
|
||||||
|
$coinField = $redpacket['coin_type'] == 1 ? 'coin' : 'earnings';
|
||||||
|
//增加余额
|
||||||
|
$addres = Db::name('user_wallet')
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->inc($coinField, $amount)
|
||||||
|
->update();
|
||||||
|
//记录用户金币日志
|
||||||
|
$data = [
|
||||||
|
'user_id' => $userId,
|
||||||
|
'change_value' => $amount,
|
||||||
|
'room_id' => $redpacket['room_id'],
|
||||||
|
'money_type' => $redpacket['coin_type'],
|
||||||
|
'change_type' => 66,//抢红包收入
|
||||||
|
'from_id' => $redpacket['room_id'],
|
||||||
|
'remarks' => '抢红包收入',
|
||||||
|
'createtime' => time()
|
||||||
|
];
|
||||||
|
|
||||||
|
$res = Db::name('vs_user_money_log')->insert($data);
|
||||||
|
if(!$res || !$addres){
|
||||||
|
Db::rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新红包剩余数量和金额
|
||||||
|
Db::name('redpacket')
|
||||||
|
->where('id', $redpacketId)
|
||||||
|
->dec('left_amount', $amount)
|
||||||
|
->dec('left_count', 1)
|
||||||
|
->update();
|
||||||
|
|
||||||
|
Db::commit();
|
||||||
|
|
||||||
|
// 获取抢红包结果详情
|
||||||
|
$grabResult = $this->getGrabResult($redpacketId, $userId);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'code' => 'grab_success',
|
||||||
|
'message' => '抢红包成功',
|
||||||
|
'data' => $grabResult
|
||||||
|
];
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
// 回滚Redis操作
|
||||||
|
$redis->hIncrByFloat($redpacketKey, 'left_amount', $amount);
|
||||||
|
$redis->hIncrBy($redpacketKey, 'left_count', 1);
|
||||||
|
$redis->sRem($userSetKey, $userId);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'code' => 'system_error',
|
||||||
|
'message' => '系统错误,请重试'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取抢红包结果详情
|
||||||
|
*/
|
||||||
|
public function getGrabResult($redpacketId, $userId)
|
||||||
|
{
|
||||||
|
// 获取红包基本信息
|
||||||
|
$redpacket = Db::name('redpacket')
|
||||||
|
->where('id', $redpacketId)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$redpacket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户的领取记录
|
||||||
|
$myRecord = Db::name('redpacket_record')
|
||||||
|
->alias('r')
|
||||||
|
->field('r.*, u.nickname, u.avatar')
|
||||||
|
->join('user u', 'u.id = r.user_id')
|
||||||
|
->where('r.redpacket_id', $redpacketId)
|
||||||
|
->where('r.user_id', $userId)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
// 获取在我之前抢到的用户记录
|
||||||
|
$previousRecords = [];
|
||||||
|
if ($myRecord) {
|
||||||
|
$previousRecords = Db::name('redpacket_record')
|
||||||
|
->alias('r')
|
||||||
|
->field('r.*, u.nickname, u.avatar')
|
||||||
|
->join('user u', 'u.id = r.user_id')
|
||||||
|
->where('r.redpacket_id', $redpacketId)
|
||||||
|
->where('r.createtime', '<', $myRecord['createtime'])
|
||||||
|
->order('r.createtime ASC')
|
||||||
|
->select();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有记录用于统计
|
||||||
|
$allRecords = Db::name('redpacket_record')
|
||||||
|
->alias('r')
|
||||||
|
->field('r.*, u.nickname, u.avatar')
|
||||||
|
->join('user u', 'u.id = r.user_id')
|
||||||
|
->where('r.redpacket_id', $redpacketId)
|
||||||
|
->order('r.createtime ASC')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
$totalGrabbed = count($allRecords);
|
||||||
|
$totalAmount = array_sum(array_column($allRecords, 'amount'));
|
||||||
|
|
||||||
|
// 手气最佳
|
||||||
|
$bestRecord = null;
|
||||||
|
if ($allRecords) {
|
||||||
|
$maxAmount = max(array_column($allRecords, 'amount'));
|
||||||
|
foreach ($allRecords as $record) {
|
||||||
|
if ($record['amount'] == $maxAmount) {
|
||||||
|
$bestRecord = $record;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'redpacket_info' => [
|
||||||
|
'id' => $redpacket['id'],
|
||||||
|
'total_amount' => $redpacket['total_amount'],
|
||||||
|
'total_count' => $redpacket['total_count'],
|
||||||
|
'left_amount' => $redpacket['left_amount'],
|
||||||
|
'left_count' => $redpacket['left_count'],
|
||||||
|
'coin_type' => $redpacket['coin_type'],
|
||||||
|
'status' => $redpacket['status']
|
||||||
|
],
|
||||||
|
'my_record' => $myRecord ? [
|
||||||
|
'amount' => $myRecord['amount'],
|
||||||
|
'createtime' => $myRecord['createtime'],
|
||||||
|
'nickname' => $myRecord['nickname'],
|
||||||
|
'avatar' => $myRecord['avatar']
|
||||||
|
] : null,
|
||||||
|
'previous_records' => $previousRecords,
|
||||||
|
'all_records' => $allRecords,
|
||||||
|
'statistics' => [
|
||||||
|
'total_grabbed' => $totalGrabbed,
|
||||||
|
'total_amount_grabbed' => $totalAmount,
|
||||||
|
'best_luck' => $bestRecord ? [
|
||||||
|
'nickname' => $bestRecord['nickname'],
|
||||||
|
'avatar' => $bestRecord['avatar'],
|
||||||
|
'amount' => $bestRecord['amount']
|
||||||
|
] : null
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查红包状态
|
||||||
|
*/
|
||||||
|
private function checkRedpacketStatus($redpacket)
|
||||||
|
{
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
if ($redpacket['status'] == Redpacket::STATUS_PENDING) {
|
||||||
|
if ($now < $redpacket['start_time']) {
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => '红包还未开始',
|
||||||
|
'data' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($redpacket['status'] == Redpacket::STATUS_FINISHED ||
|
||||||
|
$redpacket['status'] == Redpacket::STATUS_REFUNDED) {
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => '红包已结束',
|
||||||
|
'data' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($now > $redpacket['end_time']) {
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => '红包已结束',
|
||||||
|
'data' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['code' => 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否已经抢过
|
||||||
|
*/
|
||||||
|
private function hasUserGrabbed($redpacketId, $userId)
|
||||||
|
{
|
||||||
|
$record = Db::name('redpacket_record')
|
||||||
|
->where('redpacket_id', $redpacketId)
|
||||||
|
->where('user_id', $userId)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
return !empty($record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查领取条件(返回结果格式)
|
||||||
|
*/
|
||||||
|
private function checkConditionsWithResult($redpacket, $userId)
|
||||||
|
{
|
||||||
|
$conditions = $redpacket['conditions'] ? explode(',', $redpacket['conditions']): [];
|
||||||
|
|
||||||
|
if (empty($conditions)) {
|
||||||
|
return ['code' => 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array(Redpacket::CONDITION_NONE, $conditions)) {
|
||||||
|
return ['code' => 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($conditions as $condition) {
|
||||||
|
switch ($condition) {
|
||||||
|
case Redpacket::CONDITION_COLLECT_ROOM:
|
||||||
|
if (!$this->checkUserCollectedRoom($userId, $redpacket['room_id'])) {
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => '不满足收藏房间条件',
|
||||||
|
'data' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Redpacket::CONDITION_MIC_USER:
|
||||||
|
if (!$this->checkUserOnMic($userId, $redpacket['room_id'])) {
|
||||||
|
return [
|
||||||
|
'code' => 0,
|
||||||
|
'msg' => '不满足麦位用户条件',
|
||||||
|
'data' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['code' => 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抢红包
|
||||||
|
*/
|
||||||
|
// public function grab($redpacketId, $userId, $password = '')
|
||||||
|
// {
|
||||||
|
// $redpacketModel = new Redpacket();
|
||||||
|
// $redpacket = $redpacketModel->getRedpacketInfo($redpacketId);
|
||||||
|
//
|
||||||
|
// if (!$redpacket) {
|
||||||
|
// throw new \Exception('红包不存在');
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 验证口令红包
|
||||||
|
// if ($redpacket['type'] == Redpacket::TYPE_PASSWORD) {
|
||||||
|
// if (empty($password) || $password != $redpacket['password']) {
|
||||||
|
// throw new \Exception('口令错误');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 验证领取条件
|
||||||
|
// $this->checkConditions($redpacket, $userId);
|
||||||
|
//
|
||||||
|
// // 使用Redis+Lua保证原子性操作
|
||||||
|
// $redis = Cache::store('redis')->handler();
|
||||||
|
// $script = RedpacketLua::grabRedpacketScript();
|
||||||
|
//
|
||||||
|
// $redpacketKey = "redpacket:{$redpacketId}";
|
||||||
|
// $userSetKey = "redpacket_users:{$redpacketId}";
|
||||||
|
//
|
||||||
|
// $result = $redis->eval($script, [
|
||||||
|
// $redpacketKey,
|
||||||
|
// $userSetKey,
|
||||||
|
// $userId,
|
||||||
|
// time()
|
||||||
|
// ], 3);
|
||||||
|
//
|
||||||
|
// if ($result[0] == 0) {
|
||||||
|
// throw new \Exception($result[1]);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// $amount = floatval($result[1]);
|
||||||
|
//
|
||||||
|
// // Lua脚本执行成功,记录到数据库
|
||||||
|
// Db::startTrans();
|
||||||
|
// try {
|
||||||
|
// // 创建领取记录
|
||||||
|
// $recordData = [
|
||||||
|
// 'redpacket_id' => $redpacketId,
|
||||||
|
// 'user_id' => $userId,
|
||||||
|
// 'amount' => $amount
|
||||||
|
// ];
|
||||||
|
//
|
||||||
|
// $recordModel = new RedpacketRecord();
|
||||||
|
// $recordModel->save($recordData);
|
||||||
|
//
|
||||||
|
// // 更新用户钱包
|
||||||
|
// $walletModel = new UserWallet();
|
||||||
|
// $walletModel->increaseBalance($userId, $redpacket['coin_type'], $amount);
|
||||||
|
//
|
||||||
|
// // 更新红包剩余数量和金额
|
||||||
|
// Db::name('redpacket')
|
||||||
|
// ->where('id', $redpacketId)
|
||||||
|
// ->dec('left_amount', $amount)
|
||||||
|
// ->dec('left_count', 1)
|
||||||
|
// ->update();
|
||||||
|
//
|
||||||
|
// Db::commit();
|
||||||
|
//
|
||||||
|
// return $amount;
|
||||||
|
//
|
||||||
|
// } catch (\Exception $e) {
|
||||||
|
// Db::rollback();
|
||||||
|
// // 回滚Redis操作
|
||||||
|
// $redis->hIncrByFloat($redpacketKey, 'left_amount', $amount);
|
||||||
|
// $redis->hIncrBy($redpacketKey, 'left_count', 1);
|
||||||
|
// $redis->sRem($userSetKey, $userId);
|
||||||
|
// throw $e;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取红包详情和领取记录
|
||||||
|
*/
|
||||||
|
public function getDetail($redpacketId, $currentUserId = 0)
|
||||||
|
{
|
||||||
|
$redpacketModel = new Redpacket();
|
||||||
|
$redpacket = $redpacketModel->getRedpacketInfo($redpacketId);
|
||||||
|
|
||||||
|
if (!$redpacket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取领取记录
|
||||||
|
$records = Db::name('redpacket_record')
|
||||||
|
->alias('r')
|
||||||
|
->field('r.*, u.nickname, u.avatar')
|
||||||
|
->join('user u', 'u.id = r.user_id')
|
||||||
|
->where('r.redpacket_id', $redpacketId)
|
||||||
|
->order('r.createtime ASC')
|
||||||
|
->select();
|
||||||
|
|
||||||
|
$redpacket['records'] = $records;
|
||||||
|
|
||||||
|
// 检查当前用户是否已抢
|
||||||
|
$redpacket['has_grabbed'] = false;
|
||||||
|
$redpacket['my_record'] = null;
|
||||||
|
|
||||||
|
if ($currentUserId > 0) {
|
||||||
|
foreach ($records as $record) {
|
||||||
|
if ($record['user_id'] == $currentUserId) {
|
||||||
|
$redpacket['has_grabbed'] = true;
|
||||||
|
$redpacket['my_record'] = $record;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $redpacket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理过期红包退款
|
||||||
|
*/
|
||||||
|
public function processExpiredRedpackets()
|
||||||
|
{
|
||||||
|
$now = time();
|
||||||
|
$redpacketModel = new Redpacket();
|
||||||
|
|
||||||
|
// 查找已结束但未退款的红包
|
||||||
|
$expiredRedpackets = Db::name('redpacket')
|
||||||
|
->where('status', Redpacket::STATUS_ACTIVE)
|
||||||
|
->where('end_time', '<', $now)
|
||||||
|
->where('left_count', '>', 0)
|
||||||
|
->select();
|
||||||
|
|
||||||
|
foreach ($expiredRedpackets as $redpacket) {
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
// 退款给发红包用户
|
||||||
|
if ($redpacket['left_amount'] > 0) {
|
||||||
|
$walletModel = new UserWallet();
|
||||||
|
$walletModel->increaseBalance(
|
||||||
|
$redpacket['user_id'],
|
||||||
|
$redpacket['coin_type'],
|
||||||
|
$redpacket['left_amount']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新红包状态
|
||||||
|
Db::name('redpacket')
|
||||||
|
->where('id', $redpacket['id'])
|
||||||
|
->update([
|
||||||
|
'status' => Redpacket::STATUS_REFUNDED,
|
||||||
|
'updatetime' => $now
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 清理Redis缓存
|
||||||
|
$redis = Cache::store('redis')->handler();
|
||||||
|
$redisKey = "redpacket:{$redpacket['id']}";
|
||||||
|
$redis->del($redisKey);
|
||||||
|
|
||||||
|
Db::commit();
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
// 记录日志
|
||||||
|
\think\Log::error("红包退款失败: {$redpacket['id']}, 错误: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证创建红包数据
|
||||||
|
*/
|
||||||
|
private function validateCreateData($data)
|
||||||
|
{
|
||||||
|
if (empty($data['user_id'])) {
|
||||||
|
return V(0, '用户ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($data['type'], [Redpacket::TYPE_NORMAL, Redpacket::TYPE_PASSWORD])) {
|
||||||
|
return V(0, '红包类型错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data['type'] == Redpacket::TYPE_PASSWORD && empty($data['password'])) {
|
||||||
|
return V(0, '口令红包必须设置口令');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($data['coin_type'], [Redpacket::COIN_GOLD, Redpacket::COIN_DIAMOND])) {
|
||||||
|
return V(0, '币种类型错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data['total_amount'] <= 0 || $data['total_count'] <= 0) {
|
||||||
|
return V(0, '金额和数量必须大于0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($data['total_amount'] < $data['total_count']) {
|
||||||
|
return V(0, '总金额不能小于总个数');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证领取条件
|
||||||
|
if (isset($data['conditions'])) {
|
||||||
|
$res_con = $this->validateConditions($data['conditions']);
|
||||||
|
if ($res_con !== true) {
|
||||||
|
return $res_con;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证领取条件
|
||||||
|
*/
|
||||||
|
private function validateConditions($conditions)
|
||||||
|
{
|
||||||
|
if (empty($conditions)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//字符串转为数组
|
||||||
|
$conditions = explode(',', $conditions);
|
||||||
|
|
||||||
|
if (in_array(Redpacket::CONDITION_NONE, $conditions) && count($conditions) > 1) {
|
||||||
|
return V(0, '选择"无"条件时不能选择其他条件');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否满足领取条件
|
||||||
|
*/
|
||||||
|
private function checkConditions($redpacket, $userId)
|
||||||
|
{
|
||||||
|
$conditions = $redpacket['conditions'] ?: [];
|
||||||
|
|
||||||
|
if (empty($conditions)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array(Redpacket::CONDITION_NONE, $conditions)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($conditions as $condition) {
|
||||||
|
switch ($condition) {
|
||||||
|
case Redpacket::CONDITION_COLLECT_ROOM:
|
||||||
|
// 检查用户是否收藏了房间
|
||||||
|
if (!$this->checkUserCollectedRoom($userId)) {
|
||||||
|
throw new \Exception('不满足收藏房间条件');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Redpacket::CONDITION_MIC_USER:
|
||||||
|
// 检查用户是否在麦位上
|
||||||
|
if (!$this->checkUserOnMic($userId)) {
|
||||||
|
throw new \Exception('不满足麦位用户条件');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否收藏了房间(需要根据实际业务实现)
|
||||||
|
*/
|
||||||
|
private function checkUserCollectedRoom($userId,$roomId)
|
||||||
|
{
|
||||||
|
$collect = Db::name('user_follow')->where(['user_id' => $userId,'type' => 2,'follow_id' => $roomId])->find();
|
||||||
|
if (!$collect) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否在麦位上(需要根据实际业务实现)
|
||||||
|
*/
|
||||||
|
private function checkUserOnMic($userId,$roomId)
|
||||||
|
{
|
||||||
|
$onPit = Db::name('vs_room_pit')->where(['user_id' => $userId,'room_id' => $roomId])->value('pit_number');
|
||||||
|
if ($onPit <= 0){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
application/extra/redis.php
Normal file
12
application/extra/redis.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'host' => '127.0.0.1',
|
||||||
|
'port' => 6379,
|
||||||
|
'password' => '',
|
||||||
|
'select' => 0,
|
||||||
|
'timeout' => 0,
|
||||||
|
'expire' => 0,
|
||||||
|
'persistent' => false,
|
||||||
|
'prefix' => 'fa_redpacket_',
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user