increment('visit_num'); // 1. 将当前用户加入到 Redis 房间在线列表(包含 VIP 和管理员信息) $superLevel = (int) Sysparam::getValue('superlevel', '100'); $userData = [ 'level' => $user->user_level, 'sex' => $user->sex, 'headface' => $user->headface, 'vip_icon' => $user->vipIcon(), 'vip_name' => $user->vipName(), 'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '', 'is_admin' => $user->user_level >= $superLevel, ]; $this->chatState->userJoin($id, $user->username, $userData); // 2. 广播 UserJoined 事件,通知房间内的其他人 broadcast(new UserJoined($id, $user->username, $userData))->toOthers(); // 3. 获取历史消息用于初次渲染 // TODO: 可在前端通过请求另外的接口拉取历史记录,或者直接在这里 attach // 渲染主聊天框架视图 return view('chat.frame', [ 'room' => $room, 'user' => $user, ]); } /** * 发送消息 (等同于原版 NEWSAY.ASP) * * @param int $id 房间ID */ public function send(SendMessageRequest $request, int $id): JsonResponse { $data = $request->validated(); $user = Auth::user(); // 0. 检查用户是否被禁言(Redis TTL 自动过期) $muteKey = "mute:{$id}:{$user->username}"; if (Redis::exists($muteKey)) { $ttl = Redis::ttl($muteKey); $minutes = ceil($ttl / 60); return response()->json([ 'status' => 'error', 'message' => "您正在禁言中,还需等待约 {$minutes} 分钟。", ], 403); } // 1. 过滤净化消息体 $pureContent = $this->filter->filter($data['content'] ?? ''); if (empty($pureContent)) { return response()->json(['status' => 'error', 'message' => '消息内容不能为空或不合法。'], 422); } // 2. 封装消息对象 $messageData = [ 'id' => $this->chatState->nextMessageId($id), // 分布式安全自增序号 'room_id' => $id, 'from_user' => $user->username, 'to_user' => $data['to_user'] ?? '大家', 'content' => $pureContent, 'is_secret' => $data['is_secret'] ?? false, 'font_color' => $data['font_color'] ?? '', 'action' => $data['action'] ?? '', 'sent_at' => now()->toDateTimeString(), ]; // 3. 压入 Redis 缓存列表 (防炸内存,只保留最近 N 条) $this->chatState->pushMessage($id, $messageData); // 4. 立刻向 WebSocket 发射广播,前端达到 0 延迟渲染 broadcast(new MessageSent($id, $messageData)); // 5. 丢进异步列队,慢慢持久化到 MySQL,保护数据库连接池 SaveMessageJob::dispatch($messageData); // 6. 如果用户更换了字体颜色,顺便保存到 s_color 字段,下次进入时恢复 $chosenColor = $data['font_color'] ?? ''; if ($chosenColor && $chosenColor !== ($user->s_color ?? '')) { $user->s_color = $chosenColor; $user->save(); } return response()->json(['status' => 'success']); } /** * 自动挂机存点心跳与经验升级 (新增) * 替代原版定时 iframe 刷新的 save.asp。 * * @param int $id 房间ID */ public function heartbeat(Request $request, int $id): JsonResponse { $user = Auth::user(); if (! $user) { return response()->json(['status' => 'error'], 401); } // 1. 每次心跳增加经验(可在 sysparam 后台配置),VIP 倍率加成 $expGain = (int) Sysparam::getValue('exp_per_heartbeat', '1'); $expMultiplier = $this->vipService->getExpMultiplier($user); $user->exp_num += (int) round($expGain * $expMultiplier); // 2. 使用 sysparam 表中可配置的等级-经验阈值计算等级 // 管理员(superlevel 及以上)不参与自动升降级,等级由后台手动设置 $superLevel = (int) Sysparam::getValue('superlevel', '100'); $oldLevel = $user->user_level; $leveledUp = false; if ($oldLevel < $superLevel) { $newLevel = Sysparam::calculateLevel($user->exp_num); if ($newLevel !== $oldLevel && $newLevel < $superLevel) { $user->user_level = $newLevel; $leveledUp = ($newLevel > $oldLevel); } } $user->save(); // 存点入库 // 3. 将新的等级反馈给当前用户的在线名单上 // 确保刚刚升级后别人查看到的也是最准确等级 $this->chatState->userJoin($id, $user->username, [ 'level' => $user->user_level, 'sex' => $user->sex, 'headface' => $user->headface, 'vip_icon' => $user->vipIcon(), 'vip_name' => $user->vipName(), 'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '', 'is_admin' => $user->user_level >= $superLevel, ]); // 4. 如果突破境界,向全房系统喊话广播! if ($leveledUp) { // 生成炫酷广播消息发向该频道 $sysMsg = [ 'id' => $this->chatState->nextMessageId($id), 'room_id' => $id, 'from_user' => '系统传音', 'to_user' => '大家', 'content' => "🌟 天道酬勤!恭喜侠客【{$user->username}】挂机苦修,境界突破至 LV.{$user->user_level}!", 'is_secret' => false, 'font_color' => '#d97706', // 琥珀橙色 'action' => '大声宣告', 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($id, $sysMsg); broadcast(new MessageSent($id, $sysMsg)); // 落库 SaveMessageJob::dispatch($sysMsg); } // 5. 随机事件触发(复刻原版 autoact 系统,概率可在后台配置) $autoEvent = null; $eventChance = (int) Sysparam::getValue('auto_event_chance', '10'); if ($eventChance > 0 && rand(1, 100) <= $eventChance) { $autoEvent = Autoact::randomEvent(); if ($autoEvent) { // 应用经验/金币变化(不低于 0) if ($autoEvent->exp_change !== 0) { $user->exp_num = max(0, $user->exp_num + $autoEvent->exp_change); } if ($autoEvent->jjb_change !== 0) { $user->jjb = max(0, ($user->jjb ?? 0) + $autoEvent->jjb_change); } $user->save(); // 重新计算等级(经验可能因事件而变化,但管理员不参与自动升降级) if ($user->user_level < $superLevel) { $recalcLevel = Sysparam::calculateLevel($user->exp_num); if ($recalcLevel !== $user->user_level && $recalcLevel < $superLevel) { $user->user_level = $recalcLevel; $user->save(); } } // 广播随机事件消息到聊天室 $eventMsg = [ 'id' => $this->chatState->nextMessageId($id), 'room_id' => $id, 'from_user' => '星海小博士', 'to_user' => '大家', 'content' => $autoEvent->renderText($user->username), 'is_secret' => false, 'font_color' => match ($autoEvent->event_type) { 'good' => '#16a34a', // 绿色(好运) 'bad' => '#dc2626', // 红色(坏运) default => '#7c3aed', // 紫色(中性) }, 'action' => '', 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($id, $eventMsg); broadcast(new MessageSent($id, $eventMsg)); SaveMessageJob::dispatch($eventMsg); } } return response()->json([ 'status' => 'success', 'data' => [ 'exp_num' => $user->exp_num, 'user_level' => $user->user_level, 'leveled_up' => $leveledUp, 'is_max_level' => $user->user_level >= $superLevel, 'auto_event' => $autoEvent ? $autoEvent->renderText($user->username) : null, ], ]); } /** * 离开房间 (等同于原版 LEAVE.ASP) * * @param int $id 房间ID */ public function leave(Request $request, int $id): JsonResponse { $user = Auth::user(); if (! $user) { return response()->json(['status' => 'error'], 401); } // 1. 从 Redis 删除该用户 $this->chatState->userLeave($id, $user->username); // 2. 广播通知他人 broadcast(new UserLeft($id, $user->username))->toOthers(); return response()->json(['status' => 'success']); } /** * 获取可用头像列表(返回 JSON) * 扫描 /public/images/headface/ 目录,返回所有可用头像文件名 */ public function headfaceList(): JsonResponse { $dir = public_path('images/headface'); $files = []; if (is_dir($dir)) { $all = scandir($dir); foreach ($all as $file) { // 只包含图片文件 if (preg_match('/\.(gif|jpg|jpeg|png|bmp)$/i', $file)) { $files[] = $file; } } } // 自然排序(1, 2, 3... 10, 11...) natsort($files); return response()->json(['headfaces' => array_values($files)]); } /** * 修改头像(原版 fw.asp 功能) * 用户选择一个头像文件名,更新到 usersf 字段 */ public function changeAvatar(Request $request): JsonResponse { $user = auth()->user(); $headface = $request->input('headface', ''); if (empty($headface)) { return response()->json(['status' => 'error', 'message' => '请选择一个头像'], 422); } // 验证文件确实存在 if (! file_exists(public_path('images/headface/'.$headface))) { return response()->json(['status' => 'error', 'message' => '头像文件不存在'], 422); } // 更新用户头像 $user->usersf = $headface; $user->save(); // 将新头像同步到 Redis 在线用户列表中(所有房间) // 通过更新 Redis 的用户信息,使得其他用户和自己刷新后都能看到新头像 $superLevel = (int) Sysparam::getValue('superlevel', '100'); $rooms = $this->chatState->getUserRooms($user->username); foreach ($rooms as $roomId) { $this->chatState->userJoin((int) $roomId, $user->username, [ 'level' => $user->user_level, 'sex' => $user->sex, 'headface' => $headface, 'vip_icon' => $user->vipIcon(), 'vip_name' => $user->vipName(), 'vip_color' => $user->isVip() ? ($user->vipLevel?->color ?? '') : '', 'is_admin' => $user->user_level >= $superLevel, ]); } return response()->json([ 'status' => 'success', 'message' => '头像修改成功!', 'headface' => $headface, ]); } /** * 设置房间公告/祝福语(滚动显示在聊天室顶部) * 需要房间主人或等级达到 level_announcement 配置值 * * @param int $id 房间ID */ public function setAnnouncement(Request $request, int $id): JsonResponse { $user = Auth::user(); $room = Room::findOrFail($id); // 权限检查:房间主人 或 等级 >= level_announcement $requiredLevel = (int) Sysparam::getValue('level_announcement', '10'); if ($user->username !== $room->master && $user->user_level < $requiredLevel) { return response()->json(['status' => 'error', 'message' => '权限不足,无法修改公告'], 403); } $request->validate([ 'announcement' => 'required|string|max:500', ]); $room->announcement = $request->input('announcement'); $room->save(); // 广播公告更新到所有在线用户 $sysMsg = [ 'id' => $this->chatState->nextMessageId($id), 'room_id' => $id, 'from_user' => '系统公告', 'to_user' => '大家', 'content' => "📢 {$user->username} 更新了房间公告:{$room->announcement}", 'is_secret' => false, 'font_color' => '#cc0000', 'action' => '', 'sent_at' => now()->toDateTimeString(), ]; $this->chatState->pushMessage($id, $sysMsg); broadcast(new MessageSent($id, $sysMsg)); return response()->json([ 'status' => 'success', 'message' => '公告已更新!', 'announcement' => $room->announcement, ]); } }