validate([ 'username' => 'required|string', 'room_id' => 'required|integer', 'reason' => 'nullable|string|max:200', ]); $admin = Auth::user(); $targetUsername = $request->input('username'); $roomId = $request->input('room_id'); $reason = $request->input('reason', '请注意言行'); // 权限检查(等级由 level_warn 配置) if (! $this->canExecute($admin, $targetUsername, 'level_warn', '5')) { return response()->json(['status' => 'error', 'message' => '权限不足'], 403); } // 广播警告消息 $msg = [ 'id' => $this->chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => "⚠️ 管理员 {$admin->username} 警告 {$targetUsername}:{$reason}", 'is_secret' => false, 'font_color' => '#dc2626', 'action' => '', 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $msg); broadcast(new MessageSent($roomId, $msg)); SaveMessageJob::dispatch($msg); return response()->json(['status' => 'success', 'message' => "已警告 {$targetUsername}"]); } /** * 踢出用户(=T 理由) * * 将目标用户从聊天室踢出,清除其 Redis 在线状态。 * * @param Request $request 请求对象,需包含 username, room_id, reason * @return JsonResponse 操作结果 */ public function kick(Request $request): JsonResponse { $request->validate([ 'username' => 'required|string', 'room_id' => 'required|integer', 'reason' => 'nullable|string|max:200', ]); $admin = Auth::user(); $targetUsername = $request->input('username'); $roomId = $request->input('room_id'); $reason = $request->input('reason', '违反聊天室规则'); // 权限检查(等级由 level_kick 配置) if (! $this->canExecute($admin, $targetUsername, 'level_kick', '10')) { return response()->json(['status' => 'error', 'message' => '权限不足'], 403); } // 从 Redis 在线列表移除 $this->chatState->userLeave($roomId, $targetUsername); // 广播踢出消息(通知前端强制该用户跳转) $msg = [ 'id' => $this->chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => "🚫 管理员 {$admin->username} 已将 {$targetUsername} 踢出聊天室。原因:{$reason}", 'is_secret' => false, 'font_color' => '#dc2626', 'action' => '', 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $msg); broadcast(new MessageSent($roomId, $msg)); SaveMessageJob::dispatch($msg); // 广播踢出事件(前端监听后强制跳转) broadcast(new \App\Events\UserKicked($roomId, $targetUsername, $reason)); return response()->json(['status' => 'success', 'message' => "已踢出 {$targetUsername}"]); } /** * 禁言用户(=B 分钟数) * * 使用 Redis TTL 自动过期机制,禁止用户发言指定分钟数。 * * @param Request $request 请求对象,需包含 username, room_id, duration * @return JsonResponse 操作结果 */ public function mute(Request $request): JsonResponse { $request->validate([ 'username' => 'required|string', 'room_id' => 'required|integer', 'duration' => 'required|integer|min:1|max:1440', ]); $admin = Auth::user(); $targetUsername = $request->input('username'); $roomId = $request->input('room_id'); $duration = $request->input('duration'); // 权限检查(等级由 level_mute 配置) if (! $this->canExecute($admin, $targetUsername, 'level_mute', '8')) { return response()->json(['status' => 'error', 'message' => '权限不足'], 403); } // 设置 Redis 禁言标记,TTL 自动过期 $muteKey = "mute:{$roomId}:{$targetUsername}"; Redis::setex($muteKey, $duration * 60, now()->toDateTimeString()); // 广播禁言消息 $msg = [ 'id' => $this->chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => "🔇 管理员 {$admin->username} 已将 {$targetUsername} 禁言 {$duration} 分钟。", 'is_secret' => false, 'font_color' => '#dc2626', 'action' => '', 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $msg); broadcast(new MessageSent($roomId, $msg)); SaveMessageJob::dispatch($msg); // 广播禁言事件(前端禁用输入框) broadcast(new \App\Events\UserMuted( roomId: $roomId, username: $targetUsername, muteTime: $duration, operator: $admin->username, )); return response()->json(['status' => 'success', 'message' => "已禁言 {$targetUsername} {$duration} 分钟"]); } /** * 冻结用户账号(=Y 理由) * * 将用户账号状态设为冻结,禁止登录。 * * @param Request $request 请求对象,需包含 username, reason * @return JsonResponse 操作结果 */ public function freeze(Request $request): JsonResponse { $request->validate([ 'username' => 'required|string', 'room_id' => 'required|integer', 'reason' => 'nullable|string|max:200', ]); $admin = Auth::user(); $targetUsername = $request->input('username'); $roomId = $request->input('room_id'); $reason = $request->input('reason', '违反聊天室规则'); // 权限检查(等级由 level_freeze 配置) if (! $this->canExecute($admin, $targetUsername, 'level_freeze', '14')) { return response()->json(['status' => 'error', 'message' => '权限不足'], 403); } // 冻结用户账号(将等级设为 -1 表示冻结) $target = User::where('username', $targetUsername)->first(); if (! $target) { return response()->json(['status' => 'error', 'message' => '用户不存在'], 404); } $target->user_level = -1; $target->save(); // 从所有房间移除 $rooms = $this->chatState->getUserRooms($targetUsername); foreach ($rooms as $rid) { $this->chatState->userLeave($rid, $targetUsername); } // 广播冻结消息 $msg = [ 'id' => $this->chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => "🧊 管理员 {$admin->username} 已冻结 {$targetUsername} 的账号。原因:{$reason}", 'is_secret' => false, 'font_color' => '#dc2626', 'action' => '', 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $msg); broadcast(new MessageSent($roomId, $msg)); SaveMessageJob::dispatch($msg); // 广播踢出事件 broadcast(new \App\Events\UserKicked($roomId, $targetUsername, "账号已被冻结:{$reason}")); return response()->json(['status' => 'success', 'message' => "已冻结 {$targetUsername} 的账号"]); } /** * 查看用户私信(=S) * * 管理员查看指定用户最近的悄悄话记录。 * AJAX 请求返回 JSON,浏览器请求返回美观的 HTML 页面。 * * @param string $username 目标用户名 * @return JsonResponse|\Illuminate\View\View 私信记录 */ public function viewWhispers(Request $request, string $username): JsonResponse|\Illuminate\View\View { $admin = Auth::user(); $superLevel = (int) Sysparam::getValue('superlevel', '100'); if ($admin->user_level < $superLevel) { if ($request->expectsJson()) { return response()->json(['status' => 'error', 'message' => '仅站长可查看私信'], 403); } abort(403, '仅站长可查看私信'); } // 查询最近 50 条悄悄话(发送或接收) $messages = Message::where('is_secret', true) ->where(function ($q) use ($username) { $q->where('from_user', $username) ->orWhere('to_user', $username); }) ->orderByDesc('id') ->limit(50) ->get(['id', 'from_user', 'to_user', 'content', 'sent_at']); // AJAX 请求返回 JSON(给名片弹窗用),浏览器请求返回 HTML 页面 if ($request->expectsJson()) { return response()->json([ 'status' => 'success', 'username' => $username, 'messages' => $messages, ]); } return view('admin.whispers', [ 'username' => $username, 'messages' => $messages, ]); } /** * 站长公屏讲话 * * 站长发送全聊天室公告,以特殊样式显示。 * * @param Request $request 请求对象,需包含 content, room_id * @return JsonResponse 操作结果 */ public function announce(Request $request): JsonResponse { $request->validate([ 'content' => 'required|string|max:500', 'room_id' => 'required|integer', ]); $admin = Auth::user(); $superLevel = (int) Sysparam::getValue('superlevel', '100'); if ($admin->user_level < $superLevel) { return response()->json(['status' => 'error', 'message' => '仅站长可发布公屏讲话'], 403); } $roomId = $request->input('room_id'); $content = $request->input('content'); // 广播站长公告 $msg = [ 'id' => $this->chatState->nextMessageId($roomId), 'room_id' => $roomId, 'from_user' => '系统公告', 'to_user' => '大家', 'content' => "📢 站长 {$admin->username} 讲话:{$content}", 'is_secret' => false, 'font_color' => '#b91c1c', 'action' => '', 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($roomId, $msg); broadcast(new MessageSent($roomId, $msg)); SaveMessageJob::dispatch($msg); return response()->json(['status' => 'success', 'message' => '公告已发送']); } /** * 管理员全员清屏 * * 清除 Redis 中该房间的聊天记录缓存,并广播清屏事件通知所有用户前端清除消息。 * 前端只清除普通消息,保留悄悄话。 * * @param Request $request 请求对象,需包含 room_id * @return JsonResponse 操作结果 */ public function clearScreen(Request $request): JsonResponse { $request->validate([ 'room_id' => 'required|integer', ]); $admin = Auth::user(); $roomId = $request->input('room_id'); $superLevel = (int) Sysparam::getValue('superlevel', '100'); // 需要站长权限才能全员清屏 if ($admin->user_level < $superLevel) { return response()->json(['status' => 'error', 'message' => '仅站长可执行全员清屏'], 403); } // 清除 Redis 中该房间的消息缓存 $this->chatState->clearMessages($roomId); // 广播清屏事件 broadcast(new \App\Events\ScreenCleared($roomId, $admin->username)); return response()->json(['status' => 'success', 'message' => '已执行全员清屏']); } /** * 管理员触发全屏特效(烟花/下雨/雷电) * * 向房间内所有用户广播 EffectBroadcast 事件,前端收到后播放对应 Canvas 动画。 * 仅 superlevel 等级管理员可触发。 * * @param Request $request 请求对象,需包含 room_id, type * @return JsonResponse 操作结果 */ public function effect(Request $request): JsonResponse { $request->validate([ 'room_id' => 'required|integer', 'type' => 'required|in:fireworks,rain,lightning,snow', ]); $admin = Auth::user(); $roomId = $request->input('room_id'); $type = $request->input('type'); $superLevel = (int) Sysparam::getValue('superlevel', '100'); // 仅 superlevel 等级可触发特效 if ($admin->user_level < $superLevel) { return response()->json(['status' => 'error', 'message' => '仅站长可触发特效'], 403); } // 广播特效事件给房间内所有在线用户 broadcast(new \App\Events\EffectBroadcast($roomId, $type, $admin->username)); return response()->json(['status' => 'success', 'message' => "已触发特效:{$type}"]); } /** * 权限检查:管理员是否可对目标用户执行指定操作 * * 根据 sysparam 中配置的等级门槛判断权限。 * * @param User $admin 管理员用户 * @param string $targetUsername 目标用户名 * @param string $levelKey sysparam 中的等级键名(如 level_kick、level_warn) * @param string $defaultLevel 默认等级值 * @return bool 是否有权限 */ private function canExecute(User $admin, string $targetUsername, string $levelKey, string $defaultLevel = '5'): bool { // 必须达到该操作所需的最低等级 $requiredLevel = (int) Sysparam::getValue($levelKey, $defaultLevel); if ($admin->user_level < $requiredLevel) { return false; } // 不能操作自己 if ($admin->username === $targetUsername) { return false; } // 目标用户等级必须低于操作者 $target = User::where('username', $targetUsername)->first(); if ($target && $target->user_level >= $admin->user_level) { return false; } return true; } }