Files
chatroom/app/Http/Controllers/AdminCommandController.php

711 lines
27 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
/**
* 文件功能:管理员聊天室实时命令控制器
*
* 提供管理员在聊天室内对用户执行的管理操作:
* 警告(=J)、踢出(=T)、禁言(=B)、冻结(=Y)、查看私信(=S)、站长公屏讲话。
*
* 对应原 ASP 文件DOUSER.ASP / KILLUSER.ASP / LOCKIP.ASP / NEWSAY.ASP
*
* @author ChatRoom Laravel
*
* @version 1.0.0
*/
namespace App\Http\Controllers;
use App\Enums\CurrencySource;
use App\Events\MessageSent;
use App\Jobs\SaveMessageJob;
use App\Models\Message;
use App\Models\PositionAuthorityLog;
use App\Models\Sysparam;
use App\Models\User;
use App\Services\ChatStateService;
use App\Services\UserCurrencyService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Redis;
class AdminCommandController extends Controller
{
/**
* 构造函数:注入聊天状态服务
*/
public function __construct(
private readonly ChatStateService $chatState,
private readonly UserCurrencyService $currencyService,
) {}
/**
* 警告用户(=J 理由)
*
* 向目标用户发送系统警告消息,所有人可见。
*
* @param Request $request 请求对象,需包含 username, room_id, reason
* @return JsonResponse 操作结果
*/
public function warn(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_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' => "⚠️ 管理员 <b>{$admin->username}</b> 警告 <b>{$targetUsername}</b>{$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' => "🚫 管理员 <b>{$admin->username}</b> 已将 <b>{$targetUsername}</b> 踢出聊天室。原因:{$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' => "🔇 管理员 <b>{$admin->username}</b> 已将 <b>{$targetUsername}</b> 禁言 {$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' => "🧊 管理员 <b>{$admin->username}</b> 已冻结 <b>{$targetUsername}</b> 的账号。原因:{$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' => "📢 站长 <b>{$admin->username}</b> 讲话:{$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}"]);
}
/**
* 职务奖励金币(凭空发放,无需扣操作者余额)
*
* 三层限额校验:
* 1. amount ≤ position.max_reward (单次上限)
* 2. 今日累计发放 + amount ≤ position.daily_reward_limit (操作人单日累计上限)
* 3. 今日对同一接收者发放次数 < position.recipient_daily_limit同一接收者每日次数限
*
* 成功后:
* - 通过 UserCurrencyService 给接收者增加金币
* - 写入 PositionAuthorityLogaction_type=reward记录到履职记录
* - 向房间发送悄悄话通知接收者
*
* @param Request $request 需包含 username, room_id, amount
*/
public function reward(Request $request): JsonResponse
{
$request->validate([
'username' => 'required|string',
'room_id' => 'required|integer',
'amount' => 'required|integer|min:1|max:999999999',
], [
'amount.max' => '单次发放金币不能超过 999999999',
'amount.min' => '发放金币至少为 1',
'amount.integer' => '金币数量必须是整数',
'amount.required' => '请输入要发放的金币数量',
]);
$admin = Auth::user();
$roomId = (int) $request->input('room_id');
$amount = (int) $request->input('amount');
$targetUsername = $request->input('username');
// 不能给自己发放
if ($admin->username === $targetUsername) {
return response()->json(['status' => 'error', 'message' => '不能给自己发放奖励'], 422);
}
// 目标用户必须存在
$target = User::where('username', $targetUsername)->first();
if (! $target) {
return response()->json(['status' => 'error', 'message' => '用户不存在'], 404);
}
// id=1 超级管理员:无需职务,无限额限制
$isSuperAdmin = $admin->id === 1;
$userPosition = null;
$position = null;
if (! $isSuperAdmin) {
// ① 必须有在职职务
$userPosition = $admin->activePosition;
if (! $userPosition) {
return response()->json(['status' => 'error', 'message' => '你当前没有在职职务,无权发放奖励'], 403);
}
$position = $userPosition->position;
// 职务 max_reward = 0 表示禁止null 表示不限,正整数表示有上限
if ($position?->max_reward === 0) {
return response()->json(['status' => 'error', 'message' => '你的职务未配置奖励权限'], 403);
}
// ② 单次上限校验max_reward > 0 时才限制null = 不限)
if ($position->max_reward && $amount > $position->max_reward) {
return response()->json([
'status' => 'error',
'message' => "单次奖励上限为 {$position->max_reward} 金币,请调整金额",
], 422);
}
// ③ 操作人单日累计上限校验
if ($position->daily_reward_limit) {
$todayTotal = PositionAuthorityLog::where('user_id', $admin->id)
->where('action_type', 'reward')
->whereDate('created_at', today())
->sum('amount');
if ($todayTotal + $amount > $position->daily_reward_limit) {
$remaining = max(0, $position->daily_reward_limit - $todayTotal);
return response()->json([
'status' => 'error',
'message' => "今日剩余可发放额度为 {$remaining} 金币,超出单日上限({$position->daily_reward_limit}",
], 422);
}
}
// ④ 职务级别:接收者每日次数上限
if ($position->recipient_daily_limit) {
$recipientCount = PositionAuthorityLog::where('target_user_id', $target->id)
->where('action_type', 'reward')
->whereDate('created_at', today())
->count();
if ($recipientCount >= $position->recipient_daily_limit) {
return response()->json([
'status' => 'error',
'message' => "{$targetUsername} 今日已由全体职务人员累计发放 {$recipientCount} 次奖励,已达每日上限({$position->recipient_daily_limit}",
], 422);
}
}
}
// ⑤ 全局系统级别:每位用户单日最多接收奖励次数(所有操作人通用,含超管)
$globalMax = (int) Sysparam::getValue('reward_recipient_daily_max', '0');
if ($globalMax > 0) {
$globalCount = PositionAuthorityLog::where('target_user_id', $target->id)
->where('action_type', 'reward')
->whereDate('created_at', today())
->count();
if ($globalCount >= $globalMax) {
return response()->json([
'status' => 'error',
'message' => "{$targetUsername} 今日已累计接收 {$globalCount} 次奖励,已达全局每日上限({$globalMax}",
], 422);
}
}
// 发放金币(通过 UserCurrencyService 原子性更新 + 写流水)
// 组合「部门 · 职务」显示名,超管特殊处理
if ($isSuperAdmin) {
$positionName = '超级管理员';
} elseif ($position) {
$deptName = $position->department?->name;
$positionName = $deptName ? "{$deptName} · {$position->name}" : $position->name;
} else {
$positionName = '职务';
}
$this->currencyService->change(
$target,
'gold',
$amount,
CurrencySource::POSITION_REWARD,
"{$admin->username}{$positionName})职务奖励",
$roomId,
);
// 写履职记录PositionAuthorityLog超管无职务时 user_position_id 留 null
PositionAuthorityLog::create([
'user_id' => $admin->id,
'user_position_id' => $userPosition?->id,
'action_type' => 'reward',
'target_user_id' => $target->id,
'amount' => $amount,
'remark' => "发放奖励金币 {$amount} 枚给 {$targetUsername}",
]);
// ① 聊天室公开公告(所有在场用户可见)
$publicMsg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统公告',
'to_user' => '',
'content' => "💰 <b>{$admin->username}</b>{$positionName})向 <b>{$targetUsername}</b> 发放了 <b>{$amount}</b> 枚奖励金币!",
'is_secret' => false,
'font_color' => '#d97706',
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
$this->chatState->pushMessage($roomId, $publicMsg);
broadcast(new MessageSent($roomId, $publicMsg));
SaveMessageJob::dispatch($publicMsg);
// ② 接收者私信(含 toast_notification 触发右下角小卡片)
$freshJjb = $target->fresh()->jjb;
$msg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统',
'to_user' => $targetUsername,
'content' => "🎁 <b>{$admin->username}</b>{$positionName})向你发放了 <b>{$amount}</b> 枚金币奖励!当前金币:{$freshJjb} 枚。",
'is_secret' => true,
'font_color' => '#d97706',
'action' => '',
'sent_at' => now()->toDateTimeString(),
// 前端 toast-notification 组件识别此字段,弹出右下角通知卡片
'toast_notification' => [
'title' => '💰 奖励金币到账',
'message' => "<b>{$admin->username}</b>{$positionName})向你发放了 <b>{$amount}</b> 枚金币!",
'icon' => '💰',
'color' => '#f59e0b',
'duration' => 8000,
],
];
$this->chatState->pushMessage($roomId, $msg);
broadcast(new MessageSent($roomId, $msg));
SaveMessageJob::dispatch($msg);
return response()->json([
'status' => 'success',
'message' => "已向 {$targetUsername} 发放 {$amount} 金币奖励 🎉",
]);
}
/**
* 查询当前操作人的奖励额度信息(供发放弹窗展示)
*
* 返回字段:
* - max_once: 单次上限null = 不限)
* - daily_limit: 单日发放总额上限null = 不限)
* - today_sent: 今日已发放总额
* - daily_remaining: 今日剩余可发放额度null = 不限)
*/
public function rewardQuota(): \Illuminate\Http\JsonResponse
{
$admin = Auth::user();
$isSuperAdmin = $admin->id === 1;
// 最近 10 条本人发放记录(含目标用户名)
$recent = PositionAuthorityLog::with('targetUser:id,username')
->where('user_id', $admin->id)
->where('action_type', 'reward')
->latest()
->limit(10)
->get()
->map(fn ($log) => [
'target' => $log->targetUser?->username ?? '未知',
'amount' => $log->amount,
'created_at' => $log->created_at?->format('m-d H:i'),
]);
if ($isSuperAdmin) {
return response()->json([
'max_once' => null,
'daily_limit' => null,
'today_sent' => (int) PositionAuthorityLog::where('user_id', $admin->id)
->where('action_type', 'reward')
->whereDate('created_at', today())
->sum('amount'),
'daily_remaining' => null,
'recent_rewards' => $recent,
]);
}
$position = $admin->activePosition?->position;
if (! $position) {
return response()->json([
'max_once' => 0,
'daily_limit' => null,
'today_sent' => 0,
'daily_remaining' => null,
'recent_rewards' => $recent,
]);
}
// 今日已发放总额
$todaySent = (int) PositionAuthorityLog::where('user_id', $admin->id)
->where('action_type', 'reward')
->whereDate('created_at', today())
->sum('amount');
$dailyLimit = $position->daily_reward_limit;
$remaining = $dailyLimit !== null ? max(0, $dailyLimit - $todaySent) : null;
return response()->json([
'max_once' => $position->max_reward,
'daily_limit' => $dailyLimit,
'today_sent' => $todaySent,
'daily_remaining' => $remaining,
'recent_rewards' => $recent,
]);
}
/**
* 权限检查:管理员是否可对目标用户执行指定操作
*
* 根据 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;
}
}