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;
}
}