Files
midi-php/application/common/service/RedpacketService.php
2025-10-14 10:47:53 +08:00

583 lines
18 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\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)
{
$redpacketModel = new Redpacket();
$redpacket = $redpacketModel->getRedpacketInfo($redpacketId);
if (!$redpacket) {
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' => 1,
'msg' => '已经抢过该红包',
'data' => ['code' => 2] //1-抢到了2-已经抢过红包3-没有抢到
];
}
// 使用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];
if ($message == '红包已抢完') {
return [
'code' => 1,
'msg' => '手慢了,红包已抢完',
'data' => ['code' => 3] //1-抢到了2-已经抢过红包3-没有抢到
];
} elseif ($message == '已经抢过该红包') {
return [
'code' => 1,
'msg' => '已经抢过该红包',
'data' => ['code' => 2] //1-抢到了2-已经抢过红包3-没有抢到
];
}else{
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'],
//记录日志 32-发红包金币29-发红包钻石30-抢红包金币31-抢红包(钻石)
'change_type' => $redpacket['coin_type'] == 1 ? 30 : 31,
'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);
unset($grabResult['previous_records']);//前端不要
unset($grabResult['all_records']);//前端不要
unset($grabResult['statistics']);//前端不要
return [
'code' => 1,
'msg' => '抢红包成功',
// 'data' => $grabResult
// 'data' => null
'data' => ['code' => 1] //1-抢到了2-已经抢过红包3-没有抢到
];
} catch (\Exception $e) {
Db::rollback();
// 回滚Redis操作
$redis->hIncrByFloat($redpacketKey, 'left_amount', $amount);
$redis->hIncrBy($redpacketKey, 'left_count', 1);
$redis->sRem($userSetKey, $userId);
return [
'code' => 0,
'msg' => '系统错误,请重试',
'data' => null
];
}
}
/**
* 获取抢红包结果详情
*/
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'],
'nickname' => Db::name('user')->where('id', $redpacket['user_id'])->value('nickname')
],
'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 getDetail($redpacketId, $currentUserId = 0)
{
$redpacketModel = new Redpacket();
$redpacket['redpacket_info'] = $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 ['code' => 0, 'msg' => '用户ID不能为空', 'data' => null];
}
if (empty($data['room_id'])) {
return ['code' => 0, 'msg' => '房间ID不能为空', 'data' => null];
}
if (!in_array($data['type'], [Redpacket::TYPE_NORMAL, Redpacket::TYPE_PASSWORD])) {
return ['code' => 0, 'msg' => '红包类型错误', 'data' => null];
}
if ($data['type'] == Redpacket::TYPE_PASSWORD && empty($data['password'])) {
return ['code' => 0, 'msg' => '口令红包必须设置口令', 'data' => null];
}
if (!in_array($data['coin_type'], [Redpacket::COIN_GOLD, Redpacket::COIN_DIAMOND])) {
return ['code' => 0, 'msg' => '币种类型错误', 'data' => null];
}
if ($data['total_amount'] <= 0 || $data['total_count'] <= 0) {
return ['code' => 0, 'msg' => '金额和数量必须大于0', 'data' => null];
}
if ($data['total_amount'] < $data['total_count']) {
return ['code' => 0, 'msg' => '总金额不能小于总个数', 'data' => null];
}
// 验证领取条件
if (isset($data['conditions'])) {
$res_con = $this->validateConditions($data['conditions']);
if ($res_con !== true) {
return $res_con;
}
}
return ['code' => 1, 'msg' => '验证成功', 'data' => null];
}
/**
* 验证领取条件
*/
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;
}
}