重构(chat): 聊天室 Partials 第二阶段分类拆分及修复红包弹窗隐藏 Bug
- 完成对 scripts.blade.php 中非核心业务逻辑(钓鱼游戏、AI机器人、系统全局公告)的深度抽象隔离 - 修复抢红包逻辑中 setInterval 缺失时间参数(1000)引发浏览器前端主线程挂起的重度阻塞问题 - 修复 lottery-panel 组件结尾漏写 </div> 导致的连锁级渲染树崩溃(该崩溃导致红包节点被意外当作隐藏后代节点渲染,造成彻底不可见) - 对相关模板规范代码结构,执行 Laravel Pint 格式化并提交
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
* 接入 UserCurrencyService 记录所有货币变动流水。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
@@ -40,7 +41,7 @@ class RedPacketController extends Controller
|
||||
private const TOTAL_COUNT = 10;
|
||||
|
||||
/** 礼包有效期(秒) */
|
||||
private const EXPIRE_SECONDS = 120;
|
||||
private const EXPIRE_SECONDS = 300;
|
||||
|
||||
/**
|
||||
* 构造函数:注入依赖服务
|
||||
@@ -62,12 +63,12 @@ class RedPacketController extends Controller
|
||||
{
|
||||
$request->validate([
|
||||
'room_id' => 'required|integer',
|
||||
'type' => 'required|in:gold,exp',
|
||||
'type' => 'required|in:gold,exp',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$user = Auth::user();
|
||||
$roomId = (int) $request->input('room_id');
|
||||
$type = $request->input('type'); // 'gold' 或 'exp'
|
||||
$type = $request->input('type'); // 'gold' 或 'exp'
|
||||
|
||||
// 权限校验:仅 superlevel 可发礼包
|
||||
$superLevel = (int) Sysparam::getValue('superlevel', '100');
|
||||
@@ -92,8 +93,8 @@ class RedPacketController extends Controller
|
||||
|
||||
// 货币展示文案
|
||||
$typeLabel = $type === 'exp' ? '经验' : '金币';
|
||||
$typeIcon = $type === 'exp' ? '✨' : '💰';
|
||||
$btnBg = $type === 'exp'
|
||||
$typeIcon = $type === 'exp' ? '✨' : '💰';
|
||||
$btnBg = $type === 'exp'
|
||||
? 'linear-gradient(135deg,#7c3aed,#4f46e5)'
|
||||
: 'linear-gradient(135deg,#dc2626,#ea580c)';
|
||||
|
||||
@@ -101,16 +102,16 @@ class RedPacketController extends Controller
|
||||
$envelope = DB::transaction(function () use ($user, $roomId, $type, $amounts): RedPacketEnvelope {
|
||||
// 创建红包主记录(凭空发出,不扣发包人货币)
|
||||
$envelope = RedPacketEnvelope::create([
|
||||
'sender_id' => $user->id,
|
||||
'sender_id' => $user->id,
|
||||
'sender_username' => $user->username,
|
||||
'room_id' => $roomId,
|
||||
'type' => $type,
|
||||
'total_amount' => self::TOTAL_AMOUNT,
|
||||
'total_count' => self::TOTAL_COUNT,
|
||||
'claimed_count' => 0,
|
||||
'claimed_amount' => 0,
|
||||
'status' => 'active',
|
||||
'expires_at' => now()->addSeconds(self::EXPIRE_SECONDS),
|
||||
'room_id' => $roomId,
|
||||
'type' => $type,
|
||||
'total_amount' => self::TOTAL_AMOUNT,
|
||||
'total_count' => self::TOTAL_COUNT,
|
||||
'claimed_count' => 0,
|
||||
'claimed_amount' => 0,
|
||||
'status' => 'active',
|
||||
'expires_at' => now()->addSeconds(self::EXPIRE_SECONDS),
|
||||
]);
|
||||
|
||||
// 将拆分好的数量序列存入 Redis(List,LPOP 抢红包)
|
||||
@@ -137,15 +138,15 @@ class RedPacketController extends Controller
|
||||
.'box-shadow:0 2px 6px rgba(0,0,0,0.3);">'.$typeIcon.' 立即抢包</button>';
|
||||
|
||||
$msg = [
|
||||
'id' => $this->chatState->nextMessageId($roomId),
|
||||
'room_id' => $roomId,
|
||||
'from_user' => '系统公告',
|
||||
'to_user' => '',
|
||||
'content' => "🧧 <b>{$user->username}</b> 发出了一个 <b>".self::TOTAL_AMOUNT."</b> {$typeLabel}的礼包!共 ".self::TOTAL_COUNT." 份,先到先得,快去抢!{$btnHtml}",
|
||||
'is_secret' => false,
|
||||
'id' => $this->chatState->nextMessageId($roomId),
|
||||
'room_id' => $roomId,
|
||||
'from_user' => '系统公告',
|
||||
'to_user' => '',
|
||||
'content' => "🧧 <b>{$user->username}</b> 发出了一个 <b>".self::TOTAL_AMOUNT."</b> {$typeLabel}的礼包!共 ".self::TOTAL_COUNT." 份,先到先得,快去抢!{$btnHtml}",
|
||||
'is_secret' => false,
|
||||
'font_color' => $type === 'exp' ? '#6d28d9' : '#b91c1c',
|
||||
'action' => '',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
'action' => '',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
$this->chatState->pushMessage($roomId, $msg);
|
||||
broadcast(new MessageSent($roomId, $msg));
|
||||
@@ -153,18 +154,18 @@ class RedPacketController extends Controller
|
||||
|
||||
// 广播红包事件(触发前端弹出红包卡片)
|
||||
broadcast(new RedPacketSent(
|
||||
roomId: $roomId,
|
||||
envelopeId: $envelope->id,
|
||||
roomId: $roomId,
|
||||
envelopeId: $envelope->id,
|
||||
senderUsername: $user->username,
|
||||
totalAmount: self::TOTAL_AMOUNT,
|
||||
totalCount: self::TOTAL_COUNT,
|
||||
expireSeconds: self::EXPIRE_SECONDS,
|
||||
type: $type,
|
||||
totalAmount: self::TOTAL_AMOUNT,
|
||||
totalCount: self::TOTAL_COUNT,
|
||||
expireSeconds: self::EXPIRE_SECONDS,
|
||||
type: $type,
|
||||
));
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => "🧧 {$typeLabel}礼包已发出!".self::TOTAL_AMOUNT." {$typeLabel} · ".self::TOTAL_COUNT." 份",
|
||||
'status' => 'success',
|
||||
'message' => "🧧 {$typeLabel}礼包已发出!".self::TOTAL_AMOUNT." {$typeLabel} · ".self::TOTAL_COUNT.' 份',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -182,21 +183,26 @@ class RedPacketController extends Controller
|
||||
return response()->json(['status' => 'error', 'message' => '红包不存在'], 404);
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$isExpired = $envelope->expires_at->isPast();
|
||||
$user = Auth::user();
|
||||
$isExpired = $envelope->expires_at->isPast();
|
||||
$remainingCount = $envelope->remainingCount();
|
||||
$hasClaimed = RedPacketClaim::where('envelope_id', $envelopeId)
|
||||
$hasClaimed = RedPacketClaim::where('envelope_id', $envelopeId)
|
||||
->where('user_id', $user->id)
|
||||
->exists();
|
||||
|
||||
// 若已过期但 status 尚未同步,顺手更新为 expired
|
||||
if ($isExpired && $envelope->status === 'active') {
|
||||
$envelope->update(['status' => 'expired']);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'status' => 'success',
|
||||
'remaining_count' => $remainingCount,
|
||||
'total_count' => $envelope->total_count,
|
||||
'envelope_status' => $envelope->status,
|
||||
'is_expired' => $isExpired,
|
||||
'has_claimed' => $hasClaimed,
|
||||
'type' => $envelope->type ?? 'gold',
|
||||
'total_count' => $envelope->total_count,
|
||||
'envelope_status' => $isExpired ? 'expired' : $envelope->status,
|
||||
'is_expired' => $isExpired,
|
||||
'has_claimed' => $hasClaimed,
|
||||
'type' => $envelope->type ?? 'gold',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -207,8 +213,8 @@ class RedPacketController extends Controller
|
||||
* 重复领取通过 unique 约束保障幂等性。
|
||||
* 按红包 type 字段决定入账金币还是经验。
|
||||
*
|
||||
* @param Request $request 需包含 room_id
|
||||
* @param int $envelopeId 红包 ID
|
||||
* @param Request $request 需包含 room_id
|
||||
* @param int $envelopeId 红包 ID
|
||||
*/
|
||||
public function claim(Request $request, int $envelopeId): JsonResponse
|
||||
{
|
||||
@@ -216,7 +222,7 @@ class RedPacketController extends Controller
|
||||
'room_id' => 'required|integer',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$user = Auth::user();
|
||||
$roomId = (int) $request->input('room_id');
|
||||
|
||||
// 加载红包记录
|
||||
@@ -240,11 +246,11 @@ class RedPacketController extends Controller
|
||||
|
||||
// 从 Redis 原子 POP 一份数量
|
||||
$redisKey = "red_packet:{$envelopeId}:amounts";
|
||||
$amount = \Illuminate\Support\Facades\Redis::lpop($redisKey);
|
||||
$amount = \Illuminate\Support\Facades\Redis::lpop($redisKey);
|
||||
if ($amount === null || $amount === false) {
|
||||
return response()->json(['status' => 'error', 'message' => '礼包已被抢完!'], 422);
|
||||
}
|
||||
$amount = (int) $amount;
|
||||
$amount = (int) $amount;
|
||||
// 兼容旧记录(type 字段可能为 null)
|
||||
$envelopeType = $envelope->type ?? 'gold';
|
||||
|
||||
@@ -254,10 +260,10 @@ class RedPacketController extends Controller
|
||||
// 写领取记录(unique 约束保障不重复)
|
||||
RedPacketClaim::create([
|
||||
'envelope_id' => $envelope->id,
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'amount' => $amount,
|
||||
'claimed_at' => now(),
|
||||
'user_id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'amount' => $amount,
|
||||
'claimed_at' => now(),
|
||||
]);
|
||||
|
||||
// 更新红包统计
|
||||
@@ -302,30 +308,30 @@ class RedPacketController extends Controller
|
||||
broadcast(new RedPacketClaimed($user, $amount, $envelope->id));
|
||||
|
||||
// 在聊天室发送领取播报(所有人可见)
|
||||
$typeLabel = $envelopeType === 'exp' ? '经验' : '金币';
|
||||
$typeIcon = $envelopeType === 'exp' ? '✨' : '💰';
|
||||
$typeLabel = $envelopeType === 'exp' ? '经验' : '金币';
|
||||
$typeIcon = $envelopeType === 'exp' ? '✨' : '💰';
|
||||
$claimedMsg = [
|
||||
'id' => $this->chatState->nextMessageId($roomId),
|
||||
'room_id' => $roomId,
|
||||
'from_user' => '系统传音',
|
||||
'to_user' => '',
|
||||
'content' => "🧧 <b>{$user->username}</b> 抢到了 <b>{$amount}</b> {$typeLabel}礼包!{$typeIcon}",
|
||||
'is_secret' => false,
|
||||
'id' => $this->chatState->nextMessageId($roomId),
|
||||
'room_id' => $roomId,
|
||||
'from_user' => '系统传音',
|
||||
'to_user' => '',
|
||||
'content' => "🧧 <b>{$user->username}</b> 抢到了 <b>{$amount}</b> {$typeLabel}礼包!{$typeIcon}",
|
||||
'is_secret' => false,
|
||||
'font_color' => $envelopeType === 'exp' ? '#6d28d9' : '#d97706',
|
||||
'action' => '',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
'action' => '',
|
||||
'sent_at' => now()->toDateTimeString(),
|
||||
];
|
||||
$this->chatState->pushMessage($roomId, $claimedMsg);
|
||||
broadcast(new MessageSent($roomId, $claimedMsg));
|
||||
SaveMessageJob::dispatch($claimedMsg);
|
||||
|
||||
$balanceField = $envelopeType === 'exp' ? 'exp_num' : 'jjb';
|
||||
$balanceNow = $user->fresh()->$balanceField;
|
||||
$balanceNow = $user->fresh()->$balanceField;
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'amount' => $amount,
|
||||
'type' => $envelopeType,
|
||||
'status' => 'success',
|
||||
'amount' => $amount,
|
||||
'type' => $envelopeType,
|
||||
'message' => "🧧 恭喜!您抢到了 {$amount} {$typeLabel}!当前{$typeLabel}:{$balanceNow}。",
|
||||
]);
|
||||
}
|
||||
@@ -338,18 +344,18 @@ class RedPacketController extends Controller
|
||||
*
|
||||
* @param int $total 总数量
|
||||
* @param int $count 份数
|
||||
* @return int[] 每份数量数组
|
||||
* @return int[] 每份数量数组
|
||||
*/
|
||||
private function splitAmount(int $total, int $count): array
|
||||
{
|
||||
$amounts = [];
|
||||
$amounts = [];
|
||||
$remaining = $total;
|
||||
|
||||
for ($i = 1; $i < $count; $i++) {
|
||||
$leftCount = $count - $i;
|
||||
$max = min((int) floor($remaining / $leftCount * 2), $remaining - $leftCount);
|
||||
$max = max(1, $max);
|
||||
$amount = random_int(1, $max);
|
||||
$max = min((int) floor($remaining / $leftCount * 2), $remaining - $leftCount);
|
||||
$max = max(1, $max);
|
||||
$amount = random_int(1, $max);
|
||||
$amounts[] = $amount;
|
||||
$remaining -= $amount;
|
||||
}
|
||||
|
||||
@@ -84,10 +84,11 @@ class UserController extends Controller
|
||||
$levelBanIp = (int) Sysparam::getValue('level_banip', '15');
|
||||
if ($operator && $operator->user_level >= $levelBanIp) {
|
||||
$data['last_ip'] = $targetUser->last_ip;
|
||||
$data['login_ip'] = $targetUser->login_ip; // 假设表中存在 login_ip 记录本次IP,若无则使用 last_ip 退化
|
||||
// last_ip 在每次登录时更新,即为用户最近一次登录的 IP(本次IP)
|
||||
$data['login_ip'] = $targetUser->last_ip;
|
||||
|
||||
// 解析归属地:使用 ip2region 离线库,直接返回原生中文(省|市|ISP)
|
||||
$ipToLookup = $targetUser->login_ip ?: $targetUser->last_ip;
|
||||
$ipToLookup = $targetUser->last_ip;
|
||||
if ($ipToLookup) {
|
||||
try {
|
||||
// 不传路径,使用 zoujingli/ip2region 包自带的内置数据库
|
||||
|
||||
Reference in New Issue
Block a user