From 50fc8044028865366ee2ecf502228cac232699ab Mon Sep 17 00:00:00 2001 From: lkddi Date: Thu, 26 Feb 2026 13:35:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=8C=82=E6=9C=BA?= =?UTF-8?q?=E4=BF=AE=E4=BB=99=E3=80=81=E6=8E=92=E8=A1=8C=E6=A6=9C=E3=80=81?= =?UTF-8?q?=E5=A4=A7=E5=8E=85=E9=87=8D=E6=9E=84=E4=B8=8E=E5=85=A8=E7=AB=99?= =?UTF-8?q?=E7=95=99=E8=A8=80=E6=9D=BF=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - (Phase 8) 后台各维度管理与配置 - (Phase 9) 全自动静默挂机修仙升级 - (Phase 9) 四大维度风云排行榜页面 - (Phase 10) 全站留言板与悄悄话私信功能 - 运行 Pint 代码格式化 --- app/Events/MessageSent.php | 59 +++ app/Events/RoomTitleUpdated.php | 58 +++ app/Events/UserJoined.php | 60 +++ app/Events/UserKicked.php | 58 +++ app/Events/UserLeft.php | 57 ++ app/Events/UserLevelUp.php | 33 ++ app/Events/UserMuted.php | 65 +++ .../Controllers/Admin/DashboardController.php | 33 ++ app/Http/Controllers/Admin/SqlController.php | 76 +++ .../Controllers/Admin/SystemController.php | 60 +++ .../Admin/UserManagerController.php | 114 ++++ app/Http/Controllers/AuthController.php | 121 +++++ app/Http/Controllers/ChatController.php | 200 +++++++ app/Http/Controllers/GuestbookController.php | 125 +++++ .../Controllers/LeaderboardController.php | 71 +++ app/Http/Controllers/RoomController.php | 129 +++++ app/Http/Controllers/UserController.php | 149 ++++++ app/Http/Middleware/ChatAuthenticate.php | 39 ++ app/Http/Middleware/LevelRequired.php | 45 ++ app/Http/Requests/ChangePasswordRequest.php | 47 ++ app/Http/Requests/LoginRequest.php | 64 +++ app/Http/Requests/SendMessageRequest.php | 48 ++ app/Http/Requests/StoreGuestbookRequest.php | 39 ++ app/Http/Requests/StoreRoomRequest.php | 49 ++ app/Http/Requests/UpdateProfileRequest.php | 46 ++ app/Http/Requests/UpdateRoomRequest.php | 46 ++ app/Jobs/SaveMessageJob.php | 50 ++ app/Models/Action.php | 31 ++ app/Models/AdminLog.php | 60 +++ app/Models/AuditLog.php | 41 ++ app/Models/FriendCall.php | 44 ++ app/Models/FriendRequest.php | 41 ++ app/Models/Guestbook.php | 49 ++ app/Models/IpLock.php | 41 ++ app/Models/IpLog.php | 41 ++ app/Models/Marriage.php | 44 ++ app/Models/Message.php | 47 ++ app/Models/Room.php | 57 ++ app/Models/RoomDescription.php | 28 + app/Models/ScrollAd.php | 41 ++ app/Models/SysParam.php | 29 ++ app/Models/User.php | 36 +- app/Models/UserItem.php | 43 ++ app/Providers/HorizonServiceProvider.php | 4 +- app/Services/ChatStateService.php | 165 ++++++ app/Services/MessageFilterService.php | 52 ++ app/Services/UserLevelService.php | 47 ++ bootstrap/app.php | 10 +- composer.json | 1 + composer.lock | 219 +++++++- .../0001_01_01_000000_create_users_table.php | 65 ++- .../2026_02_26_040510_create_rooms_table.php | 42 ++ ...6_02_26_040521_create_audit_logs_table.php | 30 ++ ...6_02_26_040521_create_guestbooks_table.php | 37 ++ ...026_02_26_040521_create_ip_locks_table.php | 30 ++ ...026_02_26_040521_create_messages_table.php | 35 ++ ...6_02_26_040521_create_sys_params_table.php | 30 ++ ...2026_02_26_040522_create_actions_table.php | 32 ++ ...6_02_26_040522_create_admin_logs_table.php | 48 ++ ...02_26_040522_create_friend_calls_table.php | 32 ++ ...26_040522_create_friend_requests_table.php | 30 ++ ...2026_02_26_040523_create_ip_logs_table.php | 30 ++ ...26_02_26_040523_create_marriages_table.php | 33 ++ ..._040523_create_room_descriptions_table.php | 29 ++ ...6_02_26_040523_create_scroll_ads_table.php | 30 ++ ...6_02_26_040523_create_user_items_table.php | 32 ++ database/seeders/DatabaseSeeder.php | 10 +- database/seeders/SysParamSeeder.php | 31 ++ package-lock.json | 18 +- package.json | 6 +- resources/js/bootstrap.js | 19 +- resources/js/chat.js | 63 +++ resources/views/admin/dashboard.blade.php | 47 ++ resources/views/admin/layouts/app.blade.php | 83 +++ resources/views/admin/sql/index.blade.php | 98 ++++ resources/views/admin/system/edit.blade.php | 44 ++ resources/views/admin/users/index.blade.php | 138 +++++ resources/views/chat/frame.blade.php | 488 ++++++++++++++++++ resources/views/guestbook/index.blade.php | 244 +++++++++ resources/views/index.blade.php | 130 +++++ resources/views/leaderboard/index.blade.php | 131 +++++ .../views/leaderboard/partials/list.blade.php | 57 ++ resources/views/rooms/index.blade.php | 434 ++++++++++++++++ routes/channels.php | 16 + routes/web.php | 72 ++- 85 files changed, 5776 insertions(+), 30 deletions(-) create mode 100644 app/Events/MessageSent.php create mode 100644 app/Events/RoomTitleUpdated.php create mode 100644 app/Events/UserJoined.php create mode 100644 app/Events/UserKicked.php create mode 100644 app/Events/UserLeft.php create mode 100644 app/Events/UserLevelUp.php create mode 100644 app/Events/UserMuted.php create mode 100644 app/Http/Controllers/Admin/DashboardController.php create mode 100644 app/Http/Controllers/Admin/SqlController.php create mode 100644 app/Http/Controllers/Admin/SystemController.php create mode 100644 app/Http/Controllers/Admin/UserManagerController.php create mode 100644 app/Http/Controllers/AuthController.php create mode 100644 app/Http/Controllers/ChatController.php create mode 100644 app/Http/Controllers/GuestbookController.php create mode 100644 app/Http/Controllers/LeaderboardController.php create mode 100644 app/Http/Controllers/RoomController.php create mode 100644 app/Http/Controllers/UserController.php create mode 100644 app/Http/Middleware/ChatAuthenticate.php create mode 100644 app/Http/Middleware/LevelRequired.php create mode 100644 app/Http/Requests/ChangePasswordRequest.php create mode 100644 app/Http/Requests/LoginRequest.php create mode 100644 app/Http/Requests/SendMessageRequest.php create mode 100644 app/Http/Requests/StoreGuestbookRequest.php create mode 100644 app/Http/Requests/StoreRoomRequest.php create mode 100644 app/Http/Requests/UpdateProfileRequest.php create mode 100644 app/Http/Requests/UpdateRoomRequest.php create mode 100644 app/Jobs/SaveMessageJob.php create mode 100644 app/Models/Action.php create mode 100644 app/Models/AdminLog.php create mode 100644 app/Models/AuditLog.php create mode 100644 app/Models/FriendCall.php create mode 100644 app/Models/FriendRequest.php create mode 100644 app/Models/Guestbook.php create mode 100644 app/Models/IpLock.php create mode 100644 app/Models/IpLog.php create mode 100644 app/Models/Marriage.php create mode 100644 app/Models/Message.php create mode 100644 app/Models/Room.php create mode 100644 app/Models/RoomDescription.php create mode 100644 app/Models/ScrollAd.php create mode 100644 app/Models/SysParam.php create mode 100644 app/Models/UserItem.php create mode 100644 app/Services/ChatStateService.php create mode 100644 app/Services/MessageFilterService.php create mode 100644 app/Services/UserLevelService.php create mode 100644 database/migrations/2026_02_26_040510_create_rooms_table.php create mode 100644 database/migrations/2026_02_26_040521_create_audit_logs_table.php create mode 100644 database/migrations/2026_02_26_040521_create_guestbooks_table.php create mode 100644 database/migrations/2026_02_26_040521_create_ip_locks_table.php create mode 100644 database/migrations/2026_02_26_040521_create_messages_table.php create mode 100644 database/migrations/2026_02_26_040521_create_sys_params_table.php create mode 100644 database/migrations/2026_02_26_040522_create_actions_table.php create mode 100644 database/migrations/2026_02_26_040522_create_admin_logs_table.php create mode 100644 database/migrations/2026_02_26_040522_create_friend_calls_table.php create mode 100644 database/migrations/2026_02_26_040522_create_friend_requests_table.php create mode 100644 database/migrations/2026_02_26_040523_create_ip_logs_table.php create mode 100644 database/migrations/2026_02_26_040523_create_marriages_table.php create mode 100644 database/migrations/2026_02_26_040523_create_room_descriptions_table.php create mode 100644 database/migrations/2026_02_26_040523_create_scroll_ads_table.php create mode 100644 database/migrations/2026_02_26_040523_create_user_items_table.php create mode 100644 database/seeders/SysParamSeeder.php create mode 100644 resources/js/chat.js create mode 100644 resources/views/admin/dashboard.blade.php create mode 100644 resources/views/admin/layouts/app.blade.php create mode 100644 resources/views/admin/sql/index.blade.php create mode 100644 resources/views/admin/system/edit.blade.php create mode 100644 resources/views/admin/users/index.blade.php create mode 100644 resources/views/chat/frame.blade.php create mode 100644 resources/views/guestbook/index.blade.php create mode 100644 resources/views/index.blade.php create mode 100644 resources/views/leaderboard/index.blade.php create mode 100644 resources/views/leaderboard/partials/list.blade.php create mode 100644 resources/views/rooms/index.blade.php diff --git a/app/Events/MessageSent.php b/app/Events/MessageSent.php new file mode 100644 index 0000000..4d26333 --- /dev/null +++ b/app/Events/MessageSent.php @@ -0,0 +1,59 @@ + + */ + public function broadcastOn(): array + { + return [ + new PresenceChannel('room.'.$this->roomId), + ]; + } + + /** + * 获取广播时的数据 + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'message' => $this->message, + ]; + } +} diff --git a/app/Events/RoomTitleUpdated.php b/app/Events/RoomTitleUpdated.php new file mode 100644 index 0000000..d004deb --- /dev/null +++ b/app/Events/RoomTitleUpdated.php @@ -0,0 +1,58 @@ + + */ + public function broadcastOn(): array + { + return [ + new PresenceChannel('room.'.$this->roomId), + ]; + } + + /** + * 获取广播时的数据 + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'title' => $this->title, + 'message' => "系统提示:房间标题已变更为「{$this->title}」", + ]; + } +} diff --git a/app/Events/UserJoined.php b/app/Events/UserJoined.php new file mode 100644 index 0000000..b82be80 --- /dev/null +++ b/app/Events/UserJoined.php @@ -0,0 +1,60 @@ + + */ + public function broadcastOn(): array + { + return [ + new PresenceChannel('room.'.$this->roomId), + ]; + } + + /** + * 获取广播时的数据 + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'username' => $this->username, + 'info' => $this->userInfo, + ]; + } +} diff --git a/app/Events/UserKicked.php b/app/Events/UserKicked.php new file mode 100644 index 0000000..7f791c6 --- /dev/null +++ b/app/Events/UserKicked.php @@ -0,0 +1,58 @@ + + */ + public function broadcastOn(): array + { + return [ + new PresenceChannel('room.'.$this->roomId), + ]; + } + + /** + * 获取广播时的数据 + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'username' => $this->username, + 'message' => "用户 [{$this->username}] 已被踢出聊天室。", + ]; + } +} diff --git a/app/Events/UserLeft.php b/app/Events/UserLeft.php new file mode 100644 index 0000000..3d9c21e --- /dev/null +++ b/app/Events/UserLeft.php @@ -0,0 +1,57 @@ + + */ + public function broadcastOn(): array + { + return [ + new PresenceChannel('room.'.$this->roomId), + ]; + } + + /** + * 获取广播时的数据 + * + * @return array + */ + public function broadcastWith(): array + { + return [ + 'username' => $this->username, + ]; + } +} diff --git a/app/Events/UserLevelUp.php b/app/Events/UserLevelUp.php new file mode 100644 index 0000000..2aa9ccd --- /dev/null +++ b/app/Events/UserLevelUp.php @@ -0,0 +1,33 @@ + + */ + public function broadcastOn(): array + { + return [ + new PrivateChannel('channel-name'), + ]; + } +} diff --git a/app/Events/UserMuted.php b/app/Events/UserMuted.php new file mode 100644 index 0000000..d933409 --- /dev/null +++ b/app/Events/UserMuted.php @@ -0,0 +1,65 @@ + + */ + public function broadcastOn(): array + { + return [ + new PresenceChannel('room.'.$this->roomId), + ]; + } + + /** + * 获取广播时的数据 + * + * @return array + */ + public function broadcastWith(): array + { + $statusMessage = $this->muteTime > 0 + ? "用户 [{$this->username}] 已被系统封口 {$this->muteTime} 次发言时间。" + : "用户 [{$this->username}] 已被解除封口。"; + + return [ + 'username' => $this->username, + 'mute_time' => $this->muteTime, + 'message' => $statusMessage, + ]; + } +} diff --git a/app/Http/Controllers/Admin/DashboardController.php b/app/Http/Controllers/Admin/DashboardController.php new file mode 100644 index 0000000..905e9cd --- /dev/null +++ b/app/Http/Controllers/Admin/DashboardController.php @@ -0,0 +1,33 @@ + User::count(), + 'total_rooms' => Room::count(), + // 更多统计指标以后再发掘 + ]; + + return view('admin.dashboard', compact('stats')); + } +} diff --git a/app/Http/Controllers/Admin/SqlController.php b/app/Http/Controllers/Admin/SqlController.php new file mode 100644 index 0000000..c356e85 --- /dev/null +++ b/app/Http/Controllers/Admin/SqlController.php @@ -0,0 +1,76 @@ + null, 'query' => '', 'columns' => []]); + } + + /** + * 极度受限地执行 SQL (仅限 SELECT) + */ + public function execute(Request $request): View + { + $request->validate([ + 'query' => 'required|string|min:6', + ]); + + $sql = trim($request->input('query')); + + // 安全拦截:绝不允许含有 update/delete/insert/truncate/drop 等破坏性指令 + // 我们只允许查询,所以要求必须以 SELECT 起手,或者 EXPLAIN/SHOW + if (! preg_match('/^(SELECT|EXPLAIN|SHOW|DESCRIBE)\s/i', $sql)) { + return view('admin.sql.index', [ + 'results' => null, + 'columns' => [], + 'query' => $sql, + 'error' => '安全保护触发:本探针只允许执行 SELECT / SHOW 等只读查询!', + ]); + } + + try { + $results = DB::select($sql); + + // 提取表头 + $columns = []; + if (! empty($results)) { + $firstRow = (array) $results[0]; + $columns = array_keys($firstRow); + } + + return view('admin.sql.index', [ + 'results' => $results, + 'columns' => $columns, + 'query' => $sql, + 'error' => null, + ]); + } catch (\Exception $e) { + return view('admin.sql.index', [ + 'results' => null, + 'columns' => [], + 'query' => $sql, + 'error' => 'SQL 执行发生异常: '.$e->getMessage(), + ]); + } + } +} diff --git a/app/Http/Controllers/Admin/SystemController.php b/app/Http/Controllers/Admin/SystemController.php new file mode 100644 index 0000000..4e1d337 --- /dev/null +++ b/app/Http/Controllers/Admin/SystemController.php @@ -0,0 +1,60 @@ +pluck('body', 'alias')->toArray(); + + // 为后台界面准备的文案对照 (可动态化或硬编码) + $descriptions = SysParam::all()->pluck('guidetxt', 'alias')->toArray(); + + return view('admin.system.edit', compact('params', 'descriptions')); + } + + /** + * 更新全局参数,并刷新全站 Cache 缓存 + */ + public function update(Request $request): RedirectResponse + { + $data = $request->except(['_token', '_method']); + + foreach ($data as $alias => $body) { + SysParam::updateOrCreate( + ['alias' => $alias], + ['body' => $body] + ); + + // 写入 Cache 保证极速读取 + $this->chatState->setSysParam($alias, $body); + } + + return redirect()->route('admin.system.edit')->with('success', '系统参数已成功更新并生效!'); + } +} diff --git a/app/Http/Controllers/Admin/UserManagerController.php b/app/Http/Controllers/Admin/UserManagerController.php new file mode 100644 index 0000000..9965f93 --- /dev/null +++ b/app/Http/Controllers/Admin/UserManagerController.php @@ -0,0 +1,114 @@ +filled('username')) { + $query->where('username', 'like', '%'.$request->input('username').'%'); + } + + // 分页获取用户 + $users = $query->orderBy('id', 'desc')->paginate(20); + + return view('admin.users.index', compact('users')); + } + + /** + * 修改用户资料、等级或密码 (AJAX 或表单) + */ + public function update(Request $request, int $id): JsonResponse|RedirectResponse + { + $targetUser = User::findOrFail($id); + $currentUser = Auth::user(); + + // 越权防护:不能修改 等级大于或等于自己 的目标(除非修改自己) + if ($targetUser->id !== $currentUser->id && $targetUser->user_level >= $currentUser->user_level) { + return response()->json(['status' => 'error', 'message' => '权限不足:您无法修改同级或高级管理人员资料。'], 403); + } + + $validated = $request->validate([ + 'sex' => 'sometimes|in:男,女,保密', + 'user_level' => 'sometimes|integer|min:0', + 'headface' => 'sometimes|string|max:50', + 'sign' => 'sometimes|string|max:255', + 'password' => 'nullable|string|min:6', + ]); + + // 如果传了且没超权,直接赋予 + if (isset($validated['user_level'])) { + // 不能把自己或别人提权到超过自己的等级 + if ($validated['user_level'] > $currentUser->user_level && $currentUser->id !== $targetUser->id) { + return response()->json(['status' => 'error', 'message' => '您不能将别人提升至超过您的等级!'], 403); + } + $targetUser->user_level = $validated['user_level']; + } + + if (isset($validated['sex'])) { + $targetUser->sex = $validated['sex']; + } + if (isset($validated['headface'])) { + $targetUser->headface = $validated['headface']; + } + if (isset($validated['sign'])) { + $targetUser->sign = $validated['sign']; + } + + if (! empty($validated['password'])) { + $targetUser->password = Hash::make($validated['password']); + } + + $targetUser->save(); + + if ($request->wantsJson()) { + return response()->json(['status' => 'success', 'message' => '用户资料已强行更新完毕!']); + } + + return back()->with('success', '用户资料已更新!'); + } + + /** + * 物理删除杀封用户 + */ + public function destroy(Request $request, int $id): RedirectResponse + { + $targetUser = User::findOrFail($id); + $currentUser = Auth::user(); + + // 越权防护 + if ($targetUser->id !== $currentUser->id && $targetUser->user_level >= $currentUser->user_level) { + abort(403, '权限不足:无法删除同级或高级账号!'); + } + + $targetUser->delete(); + + // 可选:触发解散名下房间等 + + return back()->with('success', '目标已被物理删除。'); + } +} diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php new file mode 100644 index 0000000..4b8994c --- /dev/null +++ b/app/Http/Controllers/AuthController.php @@ -0,0 +1,121 @@ +validated(); + $username = $credentials['username']; + $password = $credentials['password']; + $ip = $request->ip(); + + $user = User::where('username', $username)->first(); + + if ($user) { + // 用户存在,验证密码 + if (Hash::check($password, $user->password)) { + // Bcrypt 验证通过 + $this->performLogin($user, $ip); + + return response()->json(['status' => 'success', 'message' => '登录成功']); + } + + // 退化为 MD5 验证(兼容原 ASP 系统的老密码) + if (md5($password) === $user->password) { + // MD5 验证通过,升级密码为 Bcrypt + $user->password = Hash::make($password); + $user->save(); + + $this->performLogin($user, $ip); + + return response()->json(['status' => 'success', 'message' => '登录成功,且安全策略已自动升级']); + } + + // 密码错误 + return response()->json([ + 'status' => 'error', + 'message' => '密码错误,请重试。', + ], 422); + } + + // --- 核心:第一次登录即为注册 --- + + $newUser = User::create([ + 'username' => $username, + 'password' => Hash::make($password), + 'first_ip' => $ip, + 'last_ip' => $ip, + 'user_level' => 1, // 默认普通用户等级 + 'sex' => '保密', // 默认性别 + // 如果原表里还有其他必填字段,在这里初始化默认值 + ]); + + $this->performLogin($newUser, $ip); + + return response()->json(['status' => 'success', 'message' => '注册并登录成功!']); + } + + /** + * 执行实际的登录操作并记录时间、IP 等。 + */ + private function performLogin(User $user, string $ip): void + { + Auth::login($user); + + // 更新最后登录IP和时间 + $user->update([ + 'last_ip' => $ip, + 'log_time' => now(), + 'in_time' => now(), + ]); + + // 可选:将用户登录状态也同步写入原有的 IpLog 模型,以便数据归档查询 + \App\Models\IpLog::create([ + 'ip' => $ip, + 'sdate' => now(), + 'uuname' => $user->username, + ]); + } + + /** + * 退出登录 + */ + public function logout(Request $request): JsonResponse + { + if (Auth::check()) { + $user = Auth::user(); + // 记录退出时间 + $user->update(['out_time' => now()]); + } + + Auth::logout(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return response()->json(['status' => 'success', 'message' => '已成功退出。']); + } +} diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php new file mode 100644 index 0000000..7172d6f --- /dev/null +++ b/app/Http/Controllers/ChatController.php @@ -0,0 +1,200 @@ +chatState->userJoin($id, $user->username, [ + 'level' => $user->user_level, + 'sex' => $user->sex, + 'headface' => $user->headface, + ]); + + // 2. 广播 UserJoined 事件,通知房间内的其他人 + broadcast(new UserJoined($id, $user->username, [ + 'level' => $user->user_level, + 'sex' => $user->sex, + 'headface' => $user->headface, + ]))->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(); + + // 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); + + 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. 每次心跳 +1 点经验 + $user->exp_num += 1; + + // 2. 检查等级计算:设定简单粗暴的平滑算式:需要经验=等级*等级*10 + // 例如:0级->0点;1级->10点;2级->40点;3级->90点;10级->1000点 + $currentLevel = $user->user_level; + $requiredExpForNextLevel = ($currentLevel) * ($currentLevel) * 10; + + $leveledUp = false; + + if ($user->exp_num >= $requiredExpForNextLevel) { + $user->user_level += 1; + $leveledUp = true; + } + + $user->save(); // 存点入库 + + // 3. 将新的等级反馈给当前用户的在线名单上 + // 确保刚刚升级后别人查看到的也是最准确等级 + $this->chatState->userJoin($id, $user->username, [ + 'level' => $user->user_level, + 'sex' => $user->sex, + 'headface' => $user->headface, + ]); + + // 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); + } + + return response()->json([ + 'status' => 'success', + 'data' => [ + 'exp_num' => $user->exp_num, + 'user_level' => $user->user_level, + 'leveled_up' => $leveledUp, + ], + ]); + } + + /** + * 离开房间 (等同于原版 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']); + } +} diff --git a/app/Http/Controllers/GuestbookController.php b/app/Http/Controllers/GuestbookController.php new file mode 100644 index 0000000..6edf002 --- /dev/null +++ b/app/Http/Controllers/GuestbookController.php @@ -0,0 +1,125 @@ +input('tab', 'public'); + $user = Auth::user(); + + $query = Guestbook::query()->orderByDesc('id'); + + // 根据 Tab 拆分查询逻辑 + if ($tab === 'inbox') { + // 收件箱:发给自己的,无论公私 + $query->where('towho', $user->username); + } elseif ($tab === 'outbox') { + // 发件箱:自己发出去的,无论公私 + $query->where('who', $user->username); + } else { + // 默认公共墙: + // 条件 = (公开留言) 或者 (悄悄话但发件人是自己) 或者 (悄悄话但收件人是自己) + $query->where(function ($q) use ($user) { + $q->where('secret', 0) + ->orWhere('who', $user->username) + ->orWhere('towho', $user->username); + }); + } + + $messages = $query->paginate(15)->appends(['tab' => $tab]); + + // 获取收件人默认值 (比如点击他人名片的"写私信"转跳过来) + $defaultTo = $request->input('to', ''); + + return view('guestbook.index', compact('messages', 'tab', 'defaultTo')); + } + + /** + * 创建一条新留言或私信 + */ + public function store(StoreGuestbookRequest $request): RedirectResponse + { + $data = $request->validated(); + $user = Auth::user(); + + // 强力消毒文本 + $pureBody = $this->filter->filter($data['text_body']); + if (empty($pureBody)) { + return back()->withInput()->with('error', '留言内容不合法或全为敏感词被过滤!'); + } + + // 处理目标人,如果没填或者填写了"大家",则默认是 null (公共留言) + $towho = trim($data['towho'] ?? ''); + if ($towho === '大家' || empty($towho)) { + $towho = null; + } + + // 如果明确指定了人,检查一下这人存不存在 (原版可不查,但查一下体验更好) + if ($towho && ! User::where('username', $towho)->exists()) { + return back()->withInput()->with('error', "目标收件人 [{$towho}] 不存在于系统中。"); + } + + Guestbook::create([ + 'who' => $user->username, + 'towho' => $towho, + 'secret' => isset($data['secret']) ? 1 : 0, + 'text_title' => mb_substr(trim($data['text_title'] ?? ''), 0, 50), + 'text_body' => $pureBody, + 'ip' => $request->ip(), + 'post_time' => now(), // 原数据库可能用 post_time 代替了 created_at,这里两个都写保证兼容 + 'created_at' => now(), + 'updated_at' => now(), + ]); + + return back()->with('success', '飞鸽传书已成功发送!'); + } + + /** + * 删除留言 + */ + public function destroy(int $id): RedirectResponse + { + $msg = Guestbook::findOrFail($id); + $user = Auth::user(); + + // 权限校验:只能删除自己发的、发给自己的,或者自己是15级以上超管 + $canDelete = $user->username === $msg->who + || $user->username === $msg->towho + || $user->user_level >= 15; + + if (! $canDelete) { + abort(403, '越权操作:您无权擦除此留言记录!'); + } + + $msg->delete(); + + return back()->with('success', '该行留言已被抹除。'); + } +} diff --git a/app/Http/Controllers/LeaderboardController.php b/app/Http/Controllers/LeaderboardController.php new file mode 100644 index 0000000..dd7b4a3 --- /dev/null +++ b/app/Http/Controllers/LeaderboardController.php @@ -0,0 +1,71 @@ +where('user_level', '>', 0) + ->orderByDesc('user_level') + ->orderBy('id') + ->limit(20) + ->get(); + }); + + // 2. 修为榜 (以 exp_num 为尊) + $topExp = Cache::remember('leaderboard:top_exp', $ttl, function () { + return User::select('id', 'username', 'headface', 'exp_num', 'sex', 'user_level', 'sign') + ->where('exp_num', '>', 0) + ->orderByDesc('exp_num') + ->orderBy('id') + ->limit(20) + ->get(); + }); + + // 3. 财富榜 (以 jjb-交友币 为尊) + $topWealth = Cache::remember('leaderboard:top_wealth', $ttl, function () { + return User::select('id', 'username', 'headface', 'jjb', 'sex', 'user_level', 'sign') + ->where('jjb', '>', 0) + ->orderByDesc('jjb') + ->orderBy('id') + ->limit(20) + ->get(); + }); + + // 4. 魅力榜 (以 meili 为尊) + $topCharm = Cache::remember('leaderboard:top_charm', $ttl, function () { + return User::select('id', 'username', 'headface', 'meili', 'sex', 'user_level', 'sign') + ->where('meili', '>', 0) + ->orderByDesc('meili') + ->orderBy('id') + ->limit(20) + ->get(); + }); + + return view('leaderboard.index', compact('topLevels', 'topExp', 'topWealth', 'topCharm')); + } +} diff --git a/app/Http/Controllers/RoomController.php b/app/Http/Controllers/RoomController.php new file mode 100644 index 0000000..2d19aef --- /dev/null +++ b/app/Http/Controllers/RoomController.php @@ -0,0 +1,129 @@ +orderByDesc('is_system') // 系统房间排在最前面 + ->orderByDesc('id') + ->get(); + + return view('rooms.index', compact('rooms')); + } + + /** + * 创建房间 (对应 NEWROOM.ASP) + */ + public function store(StoreRoomRequest $request): RedirectResponse + { + $data = $request->validated(); + + $room = Room::create([ + 'name' => $data['name'], + 'description' => $data['description'] ?? '', + 'master' => Auth::user()->username, + 'is_system' => false, // 用户自建均为非系统房 + ]); + + return redirect()->route('rooms.index')->with('success', "聊天室 [{$room->name}] 创建成功!"); + } + + /** + * 更新房间属性 (对应 ROOMSET.ASP) + */ + public function update(UpdateRoomRequest $request, int $id): RedirectResponse + { + $room = Room::findOrFail($id); + + // 鉴权:必须是该房间的主人或者是高管 (系统级 > 15) + $user = Auth::user(); + if ($room->master !== $user->username && $user->user_level < 15) { + abort(403, '只有房主或超级管理员才能修改此房间设置。'); + } + + $data = $request->validated(); + $room->update([ + 'name' => $data['name'], + 'description' => $data['description'] ?? '', + ]); + + // 广播房间信息更新 (所有人立即可以在聊天框顶部看到) + broadcast(new RoomTitleUpdated($room->id, $room->name.($room->description ? ' - '.$room->description : ''))); + + return back()->with('success', '房间设置更新成功!'); + } + + /** + * 解散/删除房间 (对应 CUTROOM.ASP) + */ + public function destroy(int $id): RedirectResponse + { + $room = Room::findOrFail($id); + + // 鉴权:系统自带房不能被任何人删除 + if ($room->is_system) { + abort(403, '系统固定聊天室无法被删除。'); + } + + $user = Auth::user(); + if ($room->master !== $user->username && $user->user_level < 15) { + abort(403, '只有房主或超级管理员才能解散该房间。'); + } + + $room->delete(); + // 备注:如果该房间内还有人,他们应该会被前端由于 channel 无法维系而被请出,或者可以在这里再发送一个专门的踢出事件。 + + return redirect()->route('rooms.index')->with('success', "聊天室 [{$room->name}] 已被彻底解散。"); + } + + /** + * 转让房主 (对应 OVERROOM.ASP) + */ + public function transfer(int $id): RedirectResponse + { + $room = Room::findOrFail($id); + $user = Auth::user(); + + if ($room->master !== $user->username && $user->user_level < 15) { + abort(403, '只有当前房主可以进行转让。'); + } + + $targetUsername = request('target_username'); + if (empty($targetUsername)) { + return back()->with('error', '请输入目标用户的昵称。'); + } + + $targetUser = User::where('username', $targetUsername)->first(); + if (! $targetUser) { + return back()->with('error', '该用户不存在,无法转让。'); + } + + $room->update(['master' => $targetUser->username]); + + return back()->with('success', "房间已成功转让给 [{$targetUser->username}]。"); + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..b631c3c --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,149 @@ +firstOrFail(); + + // 隐藏关键信息,只返回公开资料 + return response()->json([ + 'username' => $user->username, + 'sex' => $user->sex, + 'headface' => $user->headface, + 'user_level' => $user->user_level, + 'sign' => $user->sign ?? '这个人很懒,什么都没留下。', + 'created_at' => $user->created_at->format('Y-m-d'), + ]); + } + + /** + * 修改个人资料 (对应 USERSET.ASP) + */ + public function updateProfile(UpdateProfileRequest $request): JsonResponse + { + $user = Auth::user(); + $user->update($request->validated()); + + return response()->json(['status' => 'success', 'message' => '资料更新成功。']); + } + + /** + * 修改密码 (对应 chpasswd.asp) + */ + public function changePassword(ChangePasswordRequest $request): JsonResponse + { + $user = Auth::user(); + $oldPasswordInput = $request->input('old_password'); + + // 双模式密码校验逻辑(沿用 AuthController 中策略) + $isOldPasswordCorrect = false; + + // 优先验证 Bcrypt + if (Hash::check($oldPasswordInput, $user->password)) { + $isOldPasswordCorrect = true; + } + // 降级验证旧版 MD5 + elseif (md5($oldPasswordInput) === $user->password) { + $isOldPasswordCorrect = true; + } + + if (! $isOldPasswordCorrect) { + return response()->json(['status' => 'error', 'message' => '当前密码输入不正确。'], 422); + } + + // 验证通过,覆盖为新的 Bcrypt 加密串 + $user->password = Hash::make($request->input('new_password')); + $user->save(); + + return response()->json(['status' => 'success', 'message' => '密码已成功修改。下次请使用新密码登录。']); + } + + /** + * 管理员/房主操作:踢出房间 (对应 KILLUSER.ASP) + */ + public function kick(Request $request, string $username): JsonResponse + { + $operator = Auth::user(); + $roomId = $request->input('room_id'); + + if (! $roomId) { + return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422); + } + + $room = Room::findOrFail($roomId); + + // 鉴权:操作者要是房间房主或者系统超管 + if ($room->master !== $operator->username && $operator->user_level < 15) { + return response()->json(['status' => 'error', 'message' => '权限不足,无法执行踢出操作。'], 403); + } + + $targetUser = User::where('username', $username)->first(); + if (! $targetUser) { + return response()->json(['status' => 'error', 'message' => '目标用户不存在。'], 404); + } + + // 防误伤高管 + if ($targetUser->user_level >= 15 && $operator->user_level < 15) { + return response()->json(['status' => 'error', 'message' => '权限不足,无法踢出同级或高级管理人员。'], 403); + } + + // 核心动作:向频道内所有人发送包含“某某踢出某某”的事件 + broadcast(new UserKicked($roomId, $targetUser->username, "管理员 [{$operator->username}] 将 [{$targetUser->username}] 踢出了聊天室。")); + + return response()->json(['status' => 'success', 'message' => "已成功将 {$targetUser->username} 踢出房间。"]); + } + + /** + * 管理员/具有道具者操作:禁言 (对应新加的限制功能) + */ + public function mute(Request $request, string $username): JsonResponse + { + $operator = Auth::user(); + $roomId = $request->input('room_id'); + $duration = $request->input('duration', 5); // 默认封停分钟数 + + if (! $roomId) { + return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422); + } + + $room = Room::findOrFail($roomId); + + // 此处只做简单鉴权演示,和踢人一致 + if ($room->master !== $operator->username && $operator->user_level < 15) { + return response()->json(['status' => 'error', 'message' => '权限不足,无法执行禁言操作。'], 403); + } + + // 后续可以在 Redis 中写入一个 `mute:{$username}` 并附带 `TTL` 以在后台拦截 + + // 立刻向房间发送 Muted 事件 + broadcast(new UserMuted($roomId, $username, $duration)); + + return response()->json(['status' => 'success', 'message' => "已对 {$username} 实施封口 {$duration} 分钟。"]); + } +} diff --git a/app/Http/Middleware/ChatAuthenticate.php b/app/Http/Middleware/ChatAuthenticate.php new file mode 100644 index 0000000..d40e8a3 --- /dev/null +++ b/app/Http/Middleware/ChatAuthenticate.php @@ -0,0 +1,39 @@ +expectsJson()) { + return response()->json(['message' => '未登录', 'status' => 'error'], 401); + } + + return redirect()->route('home')->withErrors([ + 'auth' => '请先输入昵称进入聊天室', + ]); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/LevelRequired.php b/app/Http/Middleware/LevelRequired.php new file mode 100644 index 0000000..fe2f6e3 --- /dev/null +++ b/app/Http/Middleware/LevelRequired.php @@ -0,0 +1,45 @@ +route('home'); + } + + $user = Auth::user(); + + if ($user->user_level < $level) { + if ($request->expectsJson()) { + return response()->json(['message' => '权限不足', 'status' => 'error'], 403); + } + + abort(403, '权限不足,无法执行此操作。'); + } + + return $next($request); + } +} diff --git a/app/Http/Requests/ChangePasswordRequest.php b/app/Http/Requests/ChangePasswordRequest.php new file mode 100644 index 0000000..7f96025 --- /dev/null +++ b/app/Http/Requests/ChangePasswordRequest.php @@ -0,0 +1,47 @@ +|string> + */ + public function rules(): array + { + return [ + 'old_password' => ['required', 'string'], + 'new_password' => ['required', 'string', 'min:6', 'confirmed'], + ]; + } + + public function messages(): array + { + return [ + 'old_password.required' => '请输入当前密码。', + 'new_password.required' => '新密码不能为空。', + 'new_password.min' => '新密码长度至少需要 6 位。', + 'new_password.confirmed' => '两次输入的新密码不一致。', + ]; + } +} diff --git a/app/Http/Requests/LoginRequest.php b/app/Http/Requests/LoginRequest.php new file mode 100644 index 0000000..462b9df --- /dev/null +++ b/app/Http/Requests/LoginRequest.php @@ -0,0 +1,64 @@ +|string> + */ + public function rules(): array + { + return [ + 'username' => [ + 'required', + 'string', + 'min:2', + 'max:12', + // 允许中英文数字及常见符号,但严格过滤可能引起XSS/SQL注入的危险字符:< > ' " + 'regex:/^[^<>\'"]+$/u', + ], + 'password' => ['required', 'string', 'min:1'], + 'captcha' => ['required', 'captcha'], + ]; + } + + /** + * 获取已定义验证规则的错误消息。 + * + * @return array + */ + public function messages(): array + { + return [ + 'username.required' => '必须填写用户名。', + 'username.min' => '用户名长度不得少于 2 个字符。', + 'username.max' => '用户名长度不得超过 12 个字符。', + 'username.regex' => '用户名包含非法字符(不允许使用尖括号或引号)。', + 'password.required' => '必须填写密码。', + 'password.min' => '密码长度不得少于 1 个字符。', + 'captcha.required' => '必须填写验证码。', + 'captcha.captcha' => '验证码不正确。', + ]; + } +} diff --git a/app/Http/Requests/SendMessageRequest.php b/app/Http/Requests/SendMessageRequest.php new file mode 100644 index 0000000..3e7c357 --- /dev/null +++ b/app/Http/Requests/SendMessageRequest.php @@ -0,0 +1,48 @@ +|string> + */ + public function rules(): array + { + return [ + 'content' => ['required', 'string', 'max:500'], // 防止超长文本炸服 + 'to_user' => ['nullable', 'string', 'max:50'], + 'is_secret' => ['nullable', 'boolean'], + 'font_color' => ['nullable', 'string', 'max:10'], // html color hex + 'action' => ['nullable', 'string', 'max:50'], // 动作(例如:微笑着说) + ]; + } + + public function messages(): array + { + return [ + 'content.required' => '不能发送空消息。', + 'content.max' => '发言内容不能超过 500 个字符。', + ]; + } +} diff --git a/app/Http/Requests/StoreGuestbookRequest.php b/app/Http/Requests/StoreGuestbookRequest.php new file mode 100644 index 0000000..9afe0f6 --- /dev/null +++ b/app/Http/Requests/StoreGuestbookRequest.php @@ -0,0 +1,39 @@ +|string> + */ + public function rules(): array + { + return [ + 'towho' => 'nullable|string|max:50', + 'secret' => 'sometimes|boolean', + 'text_title' => 'nullable|string|max:250', + 'text_body' => 'required|string|max:2000', + ]; + } + + public function messages(): array + { + return [ + 'text_body.required' => '留言内容不能为空!', + 'text_body.max' => '留言内容不能超过 2000 个字符。', + ]; + } +} diff --git a/app/Http/Requests/StoreRoomRequest.php b/app/Http/Requests/StoreRoomRequest.php new file mode 100644 index 0000000..0a0165f --- /dev/null +++ b/app/Http/Requests/StoreRoomRequest.php @@ -0,0 +1,49 @@ += 10)才可以自己建房 + // 具体阈值可以根据运营需求调整,此处暂设 10 为门槛。 + return Auth::check() && Auth::user()->user_level >= 10; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:50', 'unique:rooms,name'], + 'description' => ['nullable', 'string', 'max:255'], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => '必须填写房间名称。', + 'name.unique' => '该房间名称已被占用。', + 'name.max' => '房间名称最多 50 个字符。', + ]; + } +} diff --git a/app/Http/Requests/UpdateProfileRequest.php b/app/Http/Requests/UpdateProfileRequest.php new file mode 100644 index 0000000..84f715d --- /dev/null +++ b/app/Http/Requests/UpdateProfileRequest.php @@ -0,0 +1,46 @@ +|string> + */ + public function rules(): array + { + return [ + 'sex' => ['required', 'string', 'in:男,女,保密'], + 'headface' => ['required', 'string', 'max:50'], // 比如存放 01.gif - 50.gif + 'sign' => ['nullable', 'string', 'max:255'], + ]; + } + + public function messages(): array + { + return [ + 'sex.in' => '性别选项无效。', + 'headface.required' => '必须选择一个头像。', + ]; + } +} diff --git a/app/Http/Requests/UpdateRoomRequest.php b/app/Http/Requests/UpdateRoomRequest.php new file mode 100644 index 0000000..67b39ae --- /dev/null +++ b/app/Http/Requests/UpdateRoomRequest.php @@ -0,0 +1,46 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:50', 'unique:rooms,name,'.$this->route('id')], + 'description' => ['nullable', 'string', 'max:255'], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => '房间名称不能为空。', + 'name.unique' => '该房间名称已存在。', + ]; + } +} diff --git a/app/Jobs/SaveMessageJob.php b/app/Jobs/SaveMessageJob.php new file mode 100644 index 0000000..fcbc661 --- /dev/null +++ b/app/Jobs/SaveMessageJob.php @@ -0,0 +1,50 @@ + $this->messageData['room_id'], + 'from_user' => $this->messageData['from_user'], + 'to_user' => $this->messageData['to_user'] ?? '大家', + 'content' => $this->messageData['content'], + 'is_secret' => $this->messageData['is_secret'] ?? false, + 'font_color' => $this->messageData['font_color'] ?? '', + 'action' => $this->messageData['action'] ?? '', + // 恢复 Carbon 时间对象 + 'sent_at' => Carbon::parse($this->messageData['sent_at']), + ]); + } +} diff --git a/app/Models/Action.php b/app/Models/Action.php new file mode 100644 index 0000000..52dd42e --- /dev/null +++ b/app/Models/Action.php @@ -0,0 +1,31 @@ + + */ + protected $fillable = [ + 'act_name', + 'alias', + 'toall', + 'toself', + 'toother', + ]; +} diff --git a/app/Models/AdminLog.php b/app/Models/AdminLog.php new file mode 100644 index 0000000..73dac78 --- /dev/null +++ b/app/Models/AdminLog.php @@ -0,0 +1,60 @@ + + */ + protected $fillable = [ + 'username', + 'uu_level', + 'in_time', + 'out_time', + 'caozuo', + 'fs_sl', + 'fs_name', + 'tr_sl', + 'tr_name', + 'jg_sl', + 'jg_name', + 'dj_sl', + 'dj_name', + 'yjdj_sl', + 'yjdj_name', + 'fip_sl', + 'fip_name', + 'fjj_sl', + 'fjy_sl', + 'flh_sl', + 'jl_time', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'in_time' => 'datetime', + 'jl_time' => 'datetime', + ]; + } +} diff --git a/app/Models/AuditLog.php b/app/Models/AuditLog.php new file mode 100644 index 0000000..66eba58 --- /dev/null +++ b/app/Models/AuditLog.php @@ -0,0 +1,41 @@ + + */ + protected $fillable = [ + 'occ_time', + 'occ_env', + 'stype', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'occ_time' => 'datetime', + ]; + } +} diff --git a/app/Models/FriendCall.php b/app/Models/FriendCall.php new file mode 100644 index 0000000..1cf1a75 --- /dev/null +++ b/app/Models/FriendCall.php @@ -0,0 +1,44 @@ + + */ + protected $fillable = [ + 'who', + 'towho', + 'callmess', + 'calltime', + 'read', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'calltime' => 'datetime', + 'read' => 'boolean', + ]; + } +} diff --git a/app/Models/FriendRequest.php b/app/Models/FriendRequest.php new file mode 100644 index 0000000..710b2a2 --- /dev/null +++ b/app/Models/FriendRequest.php @@ -0,0 +1,41 @@ + + */ + protected $fillable = [ + 'who', + 'towho', + 'sub_time', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'sub_time' => 'datetime', + ]; + } +} diff --git a/app/Models/Guestbook.php b/app/Models/Guestbook.php new file mode 100644 index 0000000..c4b3dfe --- /dev/null +++ b/app/Models/Guestbook.php @@ -0,0 +1,49 @@ + + */ + protected $fillable = [ + 'who', + 'towho', + 'secret', + 'ip', + 'email', + 'web', + 'addr', + 'post_time', + 'text_title', + 'text_body', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'post_time' => 'datetime', + 'secret' => 'boolean', + ]; + } +} diff --git a/app/Models/IpLock.php b/app/Models/IpLock.php new file mode 100644 index 0000000..eb0b539 --- /dev/null +++ b/app/Models/IpLock.php @@ -0,0 +1,41 @@ + + */ + protected $fillable = [ + 'ip', + 'end_time', + 'act_level', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'end_time' => 'datetime', + ]; + } +} diff --git a/app/Models/IpLog.php b/app/Models/IpLog.php new file mode 100644 index 0000000..9681cd5 --- /dev/null +++ b/app/Models/IpLog.php @@ -0,0 +1,41 @@ + + */ + protected $fillable = [ + 'ip', + 'sdate', + 'uuname', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'sdate' => 'datetime', + ]; + } +} diff --git a/app/Models/Marriage.php b/app/Models/Marriage.php new file mode 100644 index 0000000..4f923d4 --- /dev/null +++ b/app/Models/Marriage.php @@ -0,0 +1,44 @@ + + */ + protected $fillable = [ + 'hyname', + 'hyname1', + 'hytime', + 'hygb', + 'hyjb', + 'i', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'hytime' => 'datetime', + ]; + } +} diff --git a/app/Models/Message.php b/app/Models/Message.php new file mode 100644 index 0000000..7129eff --- /dev/null +++ b/app/Models/Message.php @@ -0,0 +1,47 @@ + + */ + protected $fillable = [ + 'room_id', + 'from_user', + 'to_user', + 'content', + 'is_secret', + 'font_color', + 'action', + 'sent_at', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'sent_at' => 'datetime', + 'is_secret' => 'boolean', + ]; + } +} diff --git a/app/Models/Room.php b/app/Models/Room.php new file mode 100644 index 0000000..d016858 --- /dev/null +++ b/app/Models/Room.php @@ -0,0 +1,57 @@ + + */ + protected $fillable = [ + 'room_name', + 'room_auto', + 'room_owner', + 'room_des', + 'room_top', + 'room_title', + 'room_keep', + 'room_time', + 'room_tt', + 'room_html', + 'room_exp', + 'build_time', + 'permit_level', + 'door_open', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'room_time' => 'datetime', + 'build_time' => 'datetime', + 'room_keep' => 'boolean', + 'room_tt' => 'boolean', + 'room_html' => 'boolean', + 'door_open' => 'boolean', + ]; + } +} diff --git a/app/Models/RoomDescription.php b/app/Models/RoomDescription.php new file mode 100644 index 0000000..f5d711f --- /dev/null +++ b/app/Models/RoomDescription.php @@ -0,0 +1,28 @@ + + */ + protected $fillable = [ + 'alias', + 'describ', + ]; +} diff --git a/app/Models/ScrollAd.php b/app/Models/ScrollAd.php new file mode 100644 index 0000000..7b2f37e --- /dev/null +++ b/app/Models/ScrollAd.php @@ -0,0 +1,41 @@ + + */ + protected $fillable = [ + 'ad_title', + 'ad_link', + 'ad_new_flag', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'ad_new_flag' => 'boolean', + ]; + } +} diff --git a/app/Models/SysParam.php b/app/Models/SysParam.php new file mode 100644 index 0000000..0e91693 --- /dev/null +++ b/app/Models/SysParam.php @@ -0,0 +1,29 @@ + + */ + protected $fillable = [ + 'alias', + 'guidetxt', + 'body', + ]; +} diff --git a/app/Models/User.php b/app/Models/User.php index 749c7b7..95630ab 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -1,36 +1,52 @@ */ use HasFactory, Notifiable; /** * The attributes that are mass assignable. * - * @var list + * @var array */ protected $fillable = [ - 'name', - 'email', + 'username', 'password', + 'email', + 'sex', + 'user_level', + 'room_id', + 'first_ip', + 'last_ip', ]; /** * The attributes that should be hidden for serialization. * - * @var list + * @var array */ protected $hidden = [ 'password', 'remember_token', + 'temppass', + 'ppass', + 'userpassword', ]; /** @@ -43,6 +59,14 @@ class User extends Authenticatable return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'log_time' => 'datetime', + 'in_time' => 'datetime', + 'out_time' => 'datetime', + 'hy_time' => 'datetime', + 'xr_time' => 'datetime', + 'yx_time' => 'datetime', + 'sj' => 'datetime', + 'q3_time' => 'datetime', ]; } } diff --git a/app/Models/UserItem.php b/app/Models/UserItem.php new file mode 100644 index 0000000..9dac3ae --- /dev/null +++ b/app/Models/UserItem.php @@ -0,0 +1,43 @@ + + */ + protected $fillable = [ + 'name', + 'times', + 'gg', + 'dayy', + 'lx', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'times' => 'datetime', + ]; + } +} diff --git a/app/Providers/HorizonServiceProvider.php b/app/Providers/HorizonServiceProvider.php index 59599dc..6218493 100644 --- a/app/Providers/HorizonServiceProvider.php +++ b/app/Providers/HorizonServiceProvider.php @@ -28,9 +28,7 @@ class HorizonServiceProvider extends HorizonApplicationServiceProvider protected function gate(): void { Gate::define('viewHorizon', function ($user = null) { - return in_array(optional($user)->email, [ - // - ]); + return $user && $user->user_level >= 15; }); } } diff --git a/app/Services/ChatStateService.php b/app/Services/ChatStateService.php new file mode 100644 index 0000000..07bca52 --- /dev/null +++ b/app/Services/ChatStateService.php @@ -0,0 +1,165 @@ + $jsonInfo) { + $result[$username] = json_decode($jsonInfo, true); + } + + return $result; + } + + /** + * 将一条新发言推入 Redis 列表,并限制最大保留数量,防止内存泄漏。 + * + * @param int $roomId 房间ID + * @param array $message 发言数据包 + * @param int $maxKeep 最大保留条数 + */ + public function pushMessage(int $roomId, array $message, int $maxKeep = 100): void + { + $key = "room:{$roomId}:messages"; + Redis::rpush($key, json_encode($message, JSON_UNESCAPED_UNICODE)); + + // 仅保留最新的 $maxKeep 条,旧的自动截断弹弃 (-$maxKeep 到 -1 的区间保留) + Redis::ltrim($key, -$maxKeep, -1); + } + + /** + * 获取指定房间的新发言记录。 + * 在高频长轮询或前端断线重连拉取时使用。 + * + * @param int $roomId 房间ID + * @param int $lastId 客户端收到的最后一条发言的ID + */ + public function getNewMessages(int $roomId, int $lastId): array + { + $key = "room:{$roomId}:messages"; + $messages = Redis::lrange($key, 0, -1); // 获取当前缓存的全部 + + $newMessages = []; + foreach ($messages as $msgJson) { + $msg = json_decode($msgJson, true); + if (isset($msg['id']) && $msg['id'] > $lastId) { + $newMessages[] = $msg; + } + } + + return $newMessages; + } + + /** + * 分布式发号器:为房间内的新消息生成绝对递增的 ID。 + * 解决了 MySQL 自增在极致并发下依赖读锁的问题。 + * + * @param int $roomId 房间ID + */ + public function nextMessageId(int $roomId): int + { + $key = "room:{$roomId}:message_seq"; + + return Redis::incr($key); + } + + /** + * 读取系统参数并设置合理的缓存。 + * 替代每次都去 MySQL query 的性能损耗。 + * + * @param string $alias 别名 + */ + public function getSysParam(string $alias): ?string + { + return Cache::remember("sys_param:{$alias}", 60, function () use ($alias) { + return SysParam::where('alias', $alias)->value('body'); + }); + } + + /** + * 更新系统参数并刷新 Cache 缓存。 + * + * @param string $alias 别名 + * @param mixed $value 参数值 + */ + public function setSysParam(string $alias, mixed $value): void + { + Cache::put("sys_param:{$alias}", $value); + } + + /** + * 提供一个分布式的 Redis 互斥锁包围。 + * 防止并发抢占资源(如同时创建重名房间,同时修改某一敏感属性)。 + * + * @param string $key 锁名称 + * @param callable $callback 获得锁后执行的闭包 + * @param int $timeout 锁超时时间(秒) + * @return mixed + */ + public function withLock(string $key, callable $callback, int $timeout = 5) + { + $lockKey = "lock:{$key}"; + // 尝试获取锁,set nx ex + $isLocked = Redis::set($lockKey, 1, 'EX', $timeout, 'NX'); + + if (! $isLocked) { + // 获取锁失败,业务可自行决定抛异常或重试 + throw new \Exception("The lock {$key} is currently held by another process."); + } + + try { + return $callback(); + } finally { + Redis::del($lockKey); + } + } +} diff --git a/app/Services/MessageFilterService.php b/app/Services/MessageFilterService.php new file mode 100644 index 0000000..153e2dd --- /dev/null +++ b/app/Services/MessageFilterService.php @@ -0,0 +1,52 @@ +badWords as $word) { + if (mb_strpos($content, $word) !== false) { + // 将脏字替换为相同长度的 星号 或 提示 + $replacement = str_repeat('*', mb_strlen($word)); + $content = str_replace($word, $replacement, $content); + } + } + + // 3. 将连续的空格去重,只保留一个真正的空格 + $content = preg_replace('/\s+/', ' ', $content); + + return trim($content); + } +} diff --git a/app/Services/UserLevelService.php b/app/Services/UserLevelService.php new file mode 100644 index 0000000..8961c55 --- /dev/null +++ b/app/Services/UserLevelService.php @@ -0,0 +1,47 @@ +value('user_level'); + + return $level !== null ? $level : 1; + }); + } + + /** + * 当用户等级被管理员修改时,调用此方法清空其对应缓存。 + * + * @param string $username 用户名 + */ + public function forgetUserLevel(string $username): void + { + Cache::forget("user_level:{$username}"); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index e9df4da..af4abb8 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -11,8 +11,14 @@ return Application::configure(basePath: dirname(__DIR__)) channels: __DIR__.'/../routes/channels.php', health: '/up', ) - ->withMiddleware(function (Middleware $middleware): void { - // + ->withMiddleware(function (Middleware $middleware) { + $middleware->alias([ + 'chat.auth' => \App\Http\Middleware\ChatAuthenticate::class, + 'chat.level' => \App\Http\Middleware\LevelRequired::class, + ]); + + // 这一步是为了防止用户访问需要登录的页面时,默认被跳到原版 Laravel 未定义的 login 路由报错 + $middleware->redirectGuestsTo('/'); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/composer.json b/composer.json index 07d414c..fcd014d 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "laravel/horizon": "^5.45", "laravel/reverb": "^1.8", "laravel/tinker": "^2.10.1", + "mews/captcha": "^3.4", "predis/predis": "^3.4" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 0730724..e13de1b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6aad8905e01f3b6f841e516e084a6f15", + "content-hash": "b289aa628565e10fed1f7dc23db468e9", "packages": [ { "name": "brick/math", @@ -1229,6 +1229,150 @@ ], "time": "2025-08-22T14:27:06+00:00" }, + { + "name": "intervention/gif", + "version": "4.2.4", + "source": { + "type": "git", + "url": "https://github.com/Intervention/gif.git", + "reference": "c3598a16ebe7690cd55640c44144a9df383ea73c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/gif/zipball/c3598a16ebe7690cd55640c44144a9df383ea73c", + "reference": "c3598a16ebe7690cd55640c44144a9df383ea73c", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Gif\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Native PHP GIF Encoder/Decoder", + "homepage": "https://github.com/intervention/gif", + "keywords": [ + "animation", + "gd", + "gif", + "image" + ], + "support": { + "issues": "https://github.com/Intervention/gif/issues", + "source": "https://github.com/Intervention/gif/tree/4.2.4" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2026-01-04T09:27:23+00:00" + }, + { + "name": "intervention/image", + "version": "3.11.7", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "2159bcccff18f09d2a392679b81a82c5a003f9bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/2159bcccff18f09d2a392679b81a82c5a003f9bb", + "reference": "2159bcccff18f09d2a392679b81a82c5a003f9bb", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "intervention/gif": "^4.2", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", + "slevomat/coding-standard": "~8.0", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-exif": "Recommended to be able to read EXIF data properly." + }, + "type": "library", + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io" + } + ], + "description": "PHP Image Processing", + "homepage": "https://image.intervention.io", + "keywords": [ + "gd", + "image", + "imagick", + "resize", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/3.11.7" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + }, + { + "url": "https://ko-fi.com/interventionphp", + "type": "ko_fi" + } + ], + "time": "2026-02-19T13:11:17+00:00" + }, { "name": "laravel/framework", "version": "v12.53.0", @@ -2414,6 +2558,79 @@ ], "time": "2026-01-15T06:54:53+00:00" }, + { + "name": "mews/captcha", + "version": "3.4.7", + "source": { + "type": "git", + "url": "https://github.com/mewebstudio/captcha.git", + "reference": "2622c4f90dd621f19fe57e03e45f6f099509e839" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mewebstudio/captcha/zipball/2622c4f90dd621f19fe57e03e45f6f099509e839", + "reference": "2622c4f90dd621f19fe57e03e45f6f099509e839", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "illuminate/config": "~5|^6|^7|^8|^9|^10|^11|^12", + "illuminate/filesystem": "~5|^6|^7|^8|^9|^10|^11|^12", + "illuminate/hashing": "~5|^6|^7|^8|^9|^10|^11|^12", + "illuminate/session": "~5|^6|^7|^8|^9|^10|^11|^12", + "illuminate/support": "~5|^6|^7|^8|^9|^10|^11|^12", + "intervention/image": "^3.7", + "php": "^7.2|^8.1|^8.2|^8.3" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^8.5|^9.5.10|^10.5|^11" + }, + "type": "package", + "extra": { + "laravel": { + "aliases": { + "Captcha": "Mews\\Captcha\\Facades\\Captcha" + }, + "providers": [ + "Mews\\Captcha\\CaptchaServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Mews\\Captcha\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Muharrem ERİN", + "email": "me@mewebstudio.com", + "homepage": "https://github.com/mewebstudio", + "role": "Developer" + } + ], + "description": "Laravel 5/6/7/8/9/10/11/12 Captcha Package", + "homepage": "https://github.com/mewebstudio/captcha", + "keywords": [ + "captcha", + "laravel12 Captcha", + "laravel12 Security", + "laravel5 Security" + ], + "support": { + "issues": "https://github.com/mewebstudio/captcha/issues", + "source": "https://github.com/mewebstudio/captcha/tree/3.4.7" + }, + "time": "2025-10-11T14:42:33+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..97eeea8 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -13,10 +13,67 @@ return new class extends Migration { Schema::create('users', function (Blueprint $table) { $table->id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); + $table->string('username', 50)->unique()->comment('用户名'); + $table->string('password')->comment('密码 (MD5 or Bcrypt)'); + $table->string('email', 250)->nullable()->comment('邮箱'); + $table->tinyInteger('sex')->default(0)->comment('性别 (0保密 1男 2女)'); + $table->tinyInteger('user_level')->default(1)->comment('用户等级'); + $table->dateTime('log_time')->nullable()->comment('登录时间'); + $table->integer('visit_num')->default(0)->comment('访问次数'); + $table->dateTime('in_time')->nullable()->comment('进房时间'); + $table->tinyInteger('out_info')->default(0)->comment('退出信息'); + $table->dateTime('out_time')->nullable()->comment('退出时间'); + $table->integer('exp_num')->default(0)->index()->comment('经验值'); + $table->tinyInteger('f_size')->nullable()->comment('字体大小'); + $table->tinyInteger('l_height')->nullable()->comment('行高'); + $table->tinyInteger('n_color')->nullable()->comment('名称颜色'); + $table->integer('s_color')->nullable()->comment('发言颜色'); + $table->string('remand', 250)->nullable()->comment('密码提示问题'); + $table->string('answer', 250)->nullable()->comment('密码提示答案'); + $table->string('bgcolor', 50)->nullable()->comment('背景颜色'); + $table->string('temppass', 20)->nullable()->comment('临时密码'); + $table->string('oicq', 30)->nullable()->comment('QQ号'); + $table->tinyInteger('saved')->nullable()->comment('是否保存'); + $table->string('first_ip', 50)->nullable()->comment('首次IP'); + $table->string('last_ip', 50)->nullable()->comment('最后IP'); + $table->string('aihaos', 250)->nullable()->comment('爱好'); + $table->string('friends', 250)->nullable()->comment('好友列表'); + $table->integer('headface')->nullable()->comment('头像'); + $table->integer('room_id')->default(0)->index()->comment('所在房间'); + $table->tinyInteger('auto_update')->nullable()->comment('自动刷新'); + $table->string('ppass', 50)->nullable()->comment('二级密码'); + $table->integer('jjb')->default(0)->comment('交友币/金币'); + $table->string('love', 50)->nullable()->comment('伴侣'); + $table->string('gzdw', 50)->nullable()->comment('工作单位'); + $table->integer('photo')->nullable()->comment('照片 (对应原表中文列名照片)'); + $table->integer('hj')->default(0)->comment('呼叫状态'); + $table->string('djname', 50)->nullable()->comment('等级名称'); + $table->string('usersf', 50)->nullable()->comment('用户身份'); + $table->integer('yh')->nullable()->comment('隐身状态'); + $table->text('userpassword')->nullable()->comment('备用密码'); + $table->string('huiyuan', 50)->nullable()->comment('会员组别'); + $table->dateTime('hy_time')->nullable()->comment('会员到期时间'); + $table->string('tuijian', 50)->nullable()->comment('推荐人'); + $table->string('tuijian_ip', 50)->nullable()->comment('推荐IP'); + $table->string('xiaohai', 50)->nullable()->comment('小孩'); + $table->string('qingren', 50)->nullable()->comment('情人'); + $table->string('zhufang', 50)->nullable()->comment('住房'); + $table->string('zuoqiimg', 50)->nullable()->comment('坐骑图片'); + $table->string('zuoqi', 50)->nullable()->comment('坐骑名称'); + $table->integer('guanli')->nullable()->comment('管理员标记'); + $table->integer('meili')->nullable()->comment('魅力值'); + $table->integer('teshu')->nullable()->comment('特殊权限'); + $table->dateTime('xr_time')->nullable()->comment('仙人时间'); + $table->dateTime('yx_time')->nullable()->comment('英雄时间'); + $table->integer('q1')->nullable(); + $table->integer('q2')->nullable(); + $table->integer('q3')->nullable(); + $table->string('hua', 255)->nullable()->comment('鲜花'); + $table->dateTime('sj')->nullable()->comment('注册/更新时间'); + $table->string('pig', 255)->nullable()->comment('宠物'); + $table->text('qianming')->nullable()->comment('个性签名'); + $table->dateTime('q3_time')->nullable(); + $table->rememberToken(); $table->timestamps(); }); diff --git a/database/migrations/2026_02_26_040510_create_rooms_table.php b/database/migrations/2026_02_26_040510_create_rooms_table.php new file mode 100644 index 0000000..f8b96e6 --- /dev/null +++ b/database/migrations/2026_02_26_040510_create_rooms_table.php @@ -0,0 +1,42 @@ +id(); + $table->string('room_name', 10)->unique()->comment('房间标识/名称'); + $table->string('room_auto', 10)->nullable()->comment('房间别名/自动属性'); + $table->string('room_owner', 10)->nullable()->comment('房主'); + $table->string('room_des', 250)->nullable()->comment('房间描述'); + $table->string('room_top', 100)->nullable()->comment('置顶信息'); + $table->string('room_title', 250)->nullable()->comment('房间标题'); + $table->tinyInteger('room_keep')->default(0)->comment('是否保留'); + $table->dateTime('room_time')->nullable()->comment('建立/最后时间'); + $table->tinyInteger('room_tt')->default(0)->comment('相关开关'); + $table->tinyInteger('room_html')->default(0)->comment('是否允许HTML'); + $table->integer('room_exp')->default(0)->comment('所需经验'); + $table->dateTime('build_time')->nullable()->comment('建立时间'); + $table->tinyInteger('permit_level')->default(1)->comment('允许进入的最低等级'); + $table->tinyInteger('door_open')->default(1)->comment('大门是否开启 (1开 0关)'); + $table->integer('ooooo')->nullable()->comment('未知/扩展属性o5'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('rooms'); + } +}; diff --git a/database/migrations/2026_02_26_040521_create_audit_logs_table.php b/database/migrations/2026_02_26_040521_create_audit_logs_table.php new file mode 100644 index 0000000..6014241 --- /dev/null +++ b/database/migrations/2026_02_26_040521_create_audit_logs_table.php @@ -0,0 +1,30 @@ +id(); + $table->dateTime('occ_time')->nullable()->index()->comment('发生时间'); + $table->text('occ_env')->nullable()->comment('发生环境/详情'); + $table->tinyInteger('stype')->default(0)->comment('类别标识'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('audit_logs'); + } +}; diff --git a/database/migrations/2026_02_26_040521_create_guestbooks_table.php b/database/migrations/2026_02_26_040521_create_guestbooks_table.php new file mode 100644 index 0000000..45435d0 --- /dev/null +++ b/database/migrations/2026_02_26_040521_create_guestbooks_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('who', 50)->index()->comment('留言人'); + $table->string('towho', 50)->nullable()->index()->comment('给谁留言'); + $table->tinyInteger('secret')->default(0)->comment('是否悄悄话 (1是 0否)'); + $table->string('ip', 50)->nullable()->comment('留言者IP'); + $table->string('email', 250)->nullable()->comment('留言者邮箱'); + $table->string('web', 250)->nullable()->comment('留言者网站'); + $table->string('addr', 250)->nullable()->comment('留言者地址'); + $table->dateTime('post_time')->nullable()->index()->comment('留言时间'); + $table->string('text_title', 250)->nullable()->comment('留言标题'); + $table->text('text_body')->nullable()->comment('留言内容'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('guestbooks'); + } +}; diff --git a/database/migrations/2026_02_26_040521_create_ip_locks_table.php b/database/migrations/2026_02_26_040521_create_ip_locks_table.php new file mode 100644 index 0000000..2aa3677 --- /dev/null +++ b/database/migrations/2026_02_26_040521_create_ip_locks_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('ip', 20)->unique()->comment('封锁的IP'); + $table->dateTime('end_time')->nullable()->comment('封禁结束时间'); + $table->tinyInteger('act_level')->unsigned()->default(1)->comment('封禁级别'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ip_locks'); + } +}; diff --git a/database/migrations/2026_02_26_040521_create_messages_table.php b/database/migrations/2026_02_26_040521_create_messages_table.php new file mode 100644 index 0000000..afecdea --- /dev/null +++ b/database/migrations/2026_02_26_040521_create_messages_table.php @@ -0,0 +1,35 @@ +id(); + $table->integer('room_id')->index()->comment('房间ID'); + $table->string('from_user', 50)->index()->comment('发送者'); + $table->string('to_user', 50)->nullable()->index()->comment('接收者'); + $table->text('content')->comment('消息内容'); + $table->tinyInteger('is_secret')->default(0)->comment('是否私聊'); + $table->string('font_color', 50)->nullable()->comment('字体颜色'); + $table->string('action', 50)->nullable()->comment('动作'); + $table->dateTime('sent_at')->useCurrent()->index()->comment('发送时间'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('messages'); + } +}; diff --git a/database/migrations/2026_02_26_040521_create_sys_params_table.php b/database/migrations/2026_02_26_040521_create_sys_params_table.php new file mode 100644 index 0000000..db33cbf --- /dev/null +++ b/database/migrations/2026_02_26_040521_create_sys_params_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('alias', 50)->unique()->comment('参数别名'); + $table->text('guidetxt')->nullable()->comment('参数说明'); + $table->text('body')->nullable()->comment('参数值'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sys_params'); + } +}; diff --git a/database/migrations/2026_02_26_040522_create_actions_table.php b/database/migrations/2026_02_26_040522_create_actions_table.php new file mode 100644 index 0000000..9dc2a5e --- /dev/null +++ b/database/migrations/2026_02_26_040522_create_actions_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('act_name', 50)->unique()->comment('动作名称'); + $table->string('alias', 50)->nullable()->comment('动作别名'); + $table->string('toall', 200)->nullable()->comment('对所有人表现'); + $table->string('toself', 200)->nullable()->comment('对自己的表现'); + $table->string('toother', 200)->nullable()->comment('对其他人的表现'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('actions'); + } +}; diff --git a/database/migrations/2026_02_26_040522_create_admin_logs_table.php b/database/migrations/2026_02_26_040522_create_admin_logs_table.php new file mode 100644 index 0000000..3db8006 --- /dev/null +++ b/database/migrations/2026_02_26_040522_create_admin_logs_table.php @@ -0,0 +1,48 @@ +id(); + $table->string('username', 50)->index()->comment('管理员账号'); + $table->integer('uu_level')->nullable()->comment('管理员等级'); + $table->dateTime('in_time')->nullable()->comment('进入时间'); + $table->integer('out_time')->nullable()->comment('离开时间'); + $table->text('caozuo')->nullable()->comment('操作详情'); + $table->integer('fs_sl')->default(0)->comment('封杀数量'); + $table->text('fs_name')->nullable()->comment('封杀名单'); + $table->integer('tr_sl')->default(0)->comment('踢人数量'); + $table->text('tr_name')->nullable()->comment('踢人名单'); + $table->integer('jg_sl')->default(0)->comment('警告数量'); + $table->text('jg_name')->nullable()->comment('警告名单'); + $table->integer('dj_sl')->default(0)->comment('冻结数量'); + $table->text('dj_name')->nullable()->comment('冻结名单'); + $table->integer('yjdj_sl')->default(0)->comment('永久冻结数量'); + $table->text('yjdj_name')->nullable()->comment('永久冻结名单'); + $table->integer('fip_sl')->default(0)->comment('封IP数量'); + $table->text('fip_name')->nullable()->comment('封IP名单'); + $table->integer('fjj_sl')->default(0)->comment('发奖金数量'); + $table->integer('fjy_sl')->default(0)->comment('发经验数量'); + $table->integer('flh_sl')->default(0)->comment('发老虎数量'); + $table->dateTime('jl_time')->nullable()->index()->comment('记录时间'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('admin_logs'); + } +}; diff --git a/database/migrations/2026_02_26_040522_create_friend_calls_table.php b/database/migrations/2026_02_26_040522_create_friend_calls_table.php new file mode 100644 index 0000000..260495d --- /dev/null +++ b/database/migrations/2026_02_26_040522_create_friend_calls_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('who', 50)->index()->comment('呼叫人'); + $table->string('towho', 50)->index()->comment('被呼叫人'); + $table->text('callmess')->nullable()->comment('呼叫信息'); + $table->dateTime('calltime')->nullable()->index()->comment('呼叫时间'); + $table->tinyInteger('read')->default(0)->comment('是否已读 (1是 0否)'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('friend_calls'); + } +}; diff --git a/database/migrations/2026_02_26_040522_create_friend_requests_table.php b/database/migrations/2026_02_26_040522_create_friend_requests_table.php new file mode 100644 index 0000000..e40a1ec --- /dev/null +++ b/database/migrations/2026_02_26_040522_create_friend_requests_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('who', 50)->index()->comment('申请人'); + $table->string('towho', 50)->index()->comment('被申请人'); + $table->dateTime('sub_time')->nullable()->index()->comment('申请时间'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('friend_requests'); + } +}; diff --git a/database/migrations/2026_02_26_040523_create_ip_logs_table.php b/database/migrations/2026_02_26_040523_create_ip_logs_table.php new file mode 100644 index 0000000..1bf7b9d --- /dev/null +++ b/database/migrations/2026_02_26_040523_create_ip_logs_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('ip', 50)->index()->comment('登录IP'); + $table->dateTime('sdate')->nullable()->index()->comment('登录时间'); + $table->string('uuname', 50)->index()->comment('登录用户名'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ip_logs'); + } +}; diff --git a/database/migrations/2026_02_26_040523_create_marriages_table.php b/database/migrations/2026_02_26_040523_create_marriages_table.php new file mode 100644 index 0000000..28d92e9 --- /dev/null +++ b/database/migrations/2026_02_26_040523_create_marriages_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('hyname', 50)->index()->comment('夫'); + $table->string('hyname1', 50)->index()->comment('妻'); + $table->dateTime('hytime')->nullable()->comment('结婚时间'); + $table->string('hygb', 50)->nullable()->comment('婚姻状态/广播'); + $table->string('hyjb', 50)->nullable()->comment('婚姻级别'); + $table->integer('i')->nullable()->comment('亲密度/属性'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('marriages'); + } +}; diff --git a/database/migrations/2026_02_26_040523_create_room_descriptions_table.php b/database/migrations/2026_02_26_040523_create_room_descriptions_table.php new file mode 100644 index 0000000..7cf8477 --- /dev/null +++ b/database/migrations/2026_02_26_040523_create_room_descriptions_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('alias', 10)->unique()->comment('房间别名关联'); + $table->string('describ', 254)->nullable()->comment('房间详细描述文本'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('room_descriptions'); + } +}; diff --git a/database/migrations/2026_02_26_040523_create_scroll_ads_table.php b/database/migrations/2026_02_26_040523_create_scroll_ads_table.php new file mode 100644 index 0000000..f4cf6b5 --- /dev/null +++ b/database/migrations/2026_02_26_040523_create_scroll_ads_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('ad_title', 250)->comment('广告/公告标题'); + $table->string('ad_link', 250)->nullable()->comment('链接地址'); + $table->tinyInteger('ad_new_flag')->default(0)->comment('新窗口打开标识 (1是 0否)'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('scroll_ads'); + } +}; diff --git a/database/migrations/2026_02_26_040523_create_user_items_table.php b/database/migrations/2026_02_26_040523_create_user_items_table.php new file mode 100644 index 0000000..f738d4f --- /dev/null +++ b/database/migrations/2026_02_26_040523_create_user_items_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name', 50)->index()->comment('使用者'); + $table->dateTime('times')->nullable()->index()->comment('获得时间'); + $table->text('gg')->nullable()->comment('道具类型/说明'); + $table->integer('dayy')->nullable()->comment('道具天数/数量'); + $table->string('lx', 50)->nullable()->comment('道具种类'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_items'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 6b901f8..ea9e07c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -17,9 +17,13 @@ class DatabaseSeeder extends Seeder { // User::factory(10)->create(); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'test@example.com', + // User::factory()->create([ + // 'name' => 'Test User', + // 'email' => 'test@example.com', + // ]); + + $this->call([ + SysParamSeeder::class, ]); } } diff --git a/database/seeders/SysParamSeeder.php b/database/seeders/SysParamSeeder.php new file mode 100644 index 0000000..a6c88ab --- /dev/null +++ b/database/seeders/SysParamSeeder.php @@ -0,0 +1,31 @@ + 'sys_name', 'guidetxt' => '聊天室名称', 'body' => '飘落的流星在线聊天'], + ['alias' => 'max_people', 'guidetxt' => '最大人数限制', 'body' => '500'], + ['alias' => 'welcome_msg', 'guidetxt' => '欢迎信息', 'body' => '欢迎来到飘落的流星在线聊天室!'], + ['alias' => 'sys_notice', 'guidetxt' => '系统公告', 'body' => '系统重构中,体验Laravel 12 + Reverb全新架构。'], + ['alias' => 'name_length', 'guidetxt' => '用户名最大长度', 'body' => '10'], + ['alias' => 'say_length', 'guidetxt' => '发言最大长度', 'body' => '250'], + ]; + + foreach ($params as $param) { + SysParam::updateOrCreate( + ['alias' => $param['alias']], + $param + ); + } + } +} diff --git a/package-lock.json b/package-lock.json index e5b3029..7ba3b33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,15 +4,13 @@ "requires": true, "packages": { "": { - "dependencies": { - "laravel-echo": "^2.3.0", - "pusher-js": "^8.4.0" - }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", "axios": "^1.11.0", "concurrently": "^9.0.1", + "laravel-echo": "^2.3.0", "laravel-vite-plugin": "^2.0.0", + "pusher-js": "^8.4.0", "tailwindcss": "^4.0.0", "vite": "^7.0.7" } @@ -863,6 +861,7 @@ "version": "3.1.2", "resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, "license": "MIT", "peer": true }, @@ -1311,6 +1310,7 @@ "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -1371,6 +1371,7 @@ "version": "6.6.4", "resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.4.tgz", "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -1385,6 +1386,7 @@ "version": "5.2.3", "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz", "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -1732,6 +1734,7 @@ "version": "2.3.0", "resolved": "https://registry.npmmirror.com/laravel-echo/-/laravel-echo-2.3.0.tgz", "integrity": "sha512-wgHPnnBvfHmu2I58xJ4asZH37Nu6P0472ku6zuoGRLc3zEWwIbpovDLYTiOshDH1SM7rA6AjZTKuu+jYoM1tpQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -2069,6 +2072,7 @@ "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT", "peer": true }, @@ -2151,6 +2155,7 @@ "version": "8.4.0", "resolved": "https://registry.npmmirror.com/pusher-js/-/pusher-js-8.4.0.tgz", "integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==", + "dev": true, "license": "MIT", "dependencies": { "tweetnacl": "^1.0.3" @@ -2238,6 +2243,7 @@ "version": "4.8.3", "resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.3.tgz", "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -2254,6 +2260,7 @@ "version": "4.2.5", "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.5.tgz", "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -2377,6 +2384,7 @@ "version": "1.0.3", "resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dev": true, "license": "Unlicense" }, "node_modules/vite": { @@ -2500,6 +2508,7 @@ "version": "8.18.3", "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, "license": "MIT", "peer": true, "engines": { @@ -2522,6 +2531,7 @@ "version": "2.1.2", "resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "dev": true, "peer": true, "engines": { "node": ">=0.4.0" diff --git a/package.json b/package.json index acee125..27271e3 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,10 @@ "@tailwindcss/vite": "^4.0.0", "axios": "^1.11.0", "concurrently": "^9.0.1", + "laravel-echo": "^2.3.0", "laravel-vite-plugin": "^2.0.0", + "pusher-js": "^8.4.0", "tailwindcss": "^4.0.0", "vite": "^7.0.7" - }, - "dependencies": { - "laravel-echo": "^2.3.0", - "pusher-js": "^8.4.0" } } diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 5f1390b..8b8ecf4 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -1,4 +1,19 @@ -import axios from 'axios'; +import axios from "axios"; window.axios = axios; -window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; +window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; + +import Echo from "laravel-echo"; +import Pusher from "pusher-js"; + +window.Pusher = Pusher; + +window.Echo = new Echo({ + broadcaster: "reverb", + key: import.meta.env.VITE_REVERB_APP_KEY, + wsHost: import.meta.env.VITE_REVERB_HOST, + wsPort: import.meta.env.VITE_REVERB_PORT ?? 8080, + wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, + forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? "https") === "https", + enabledTransports: ["ws", "wss"], +}); diff --git a/resources/js/chat.js b/resources/js/chat.js new file mode 100644 index 0000000..07da39c --- /dev/null +++ b/resources/js/chat.js @@ -0,0 +1,63 @@ +import "./bootstrap"; + +// 这个文件负责处理浏览器与 Laravel Reverb WebSocket 服务器的通信。 +// 通过 Presence Channel 实现聊天室的核心监听。 + +export function initChat(roomId) { + if (!roomId) { + console.error("未提供 roomId,无法初始化 WebSocket 连接。"); + return; + } + + // 加入带有登录人员追踪的 Presence Channel + window.Echo.join(`room.${roomId}`) + // 当自己成功连接时,获取当前在这里的所有人列表 + .here((users) => { + console.log("当前房间内的在线人员:", users); + // 触发自定义事件,让具体的前端 UI 去接管渲染 + window.dispatchEvent( + new CustomEvent("chat:here", { detail: users }), + ); + }) + // 监听其他人的加入 + .joining((user) => { + console.log(user.username + " 进入了房间"); + window.dispatchEvent( + new CustomEvent("chat:joining", { detail: user }), + ); + }) + // 监听其他人的离开 + .leaving((user) => { + console.log(user.username + " 离开了房间"); + window.dispatchEvent( + new CustomEvent("chat:leaving", { detail: user }), + ); + }) + // 监听新发送的文本消息 + .listen("MessageSent", (e) => { + console.log("收到新发言:", e.message); + window.dispatchEvent( + new CustomEvent("chat:message", { detail: e.message }), + ); + }) + // 监听踢出事件(通常判断是不是自己被踢出了) + .listen("UserKicked", (e) => { + console.log("踢出通知:", e); + window.dispatchEvent(new CustomEvent("chat:kicked", { detail: e })); + }) + // 监听封口禁言事件 + .listen("UserMuted", (e) => { + console.log("禁言通知:", e); + window.dispatchEvent(new CustomEvent("chat:muted", { detail: e })); + }) + // 监听房间主题被改变 + .listen("RoomTitleUpdated", (e) => { + console.log("主题改变:", e); + window.dispatchEvent( + new CustomEvent("chat:title-updated", { detail: e }), + ); + }); +} + +// 供全局调用 +window.initChat = initChat; diff --git a/resources/views/admin/dashboard.blade.php b/resources/views/admin/dashboard.blade.php new file mode 100644 index 0000000..0be6e0c --- /dev/null +++ b/resources/views/admin/dashboard.blade.php @@ -0,0 +1,47 @@ +@extends('admin.layouts.app') + +@section('title', '仪表盘') + +@section('content') +
+
+

总计注册用户数

+

{{ $stats['total_users'] }}

+
+ +
+

总计聊天频道数

+

{{ $stats['total_rooms'] }}

+
+
+ +
+
+

系统信息摘要

+
+
+ +
+
+@endsection diff --git a/resources/views/admin/layouts/app.blade.php b/resources/views/admin/layouts/app.blade.php new file mode 100644 index 0000000..ffb02e1 --- /dev/null +++ b/resources/views/admin/layouts/app.blade.php @@ -0,0 +1,83 @@ + + + + + + + 后台管理 - 飘落流星 + + + + + + + + + +
+ +
+

@yield('title', '控制台')

+
+ 当前操作人: {{ Auth::user()->username }} +
+
+ + +
+ @if (session('success')) +
+ {{ session('success') }} +
+ @endif + @if (session('error')) +
+ {{ session('error') }} +
+ @endif + @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + + @yield('content') +
+
+ + + diff --git a/resources/views/admin/sql/index.blade.php b/resources/views/admin/sql/index.blade.php new file mode 100644 index 0000000..a3ccadf --- /dev/null +++ b/resources/views/admin/sql/index.blade.php @@ -0,0 +1,98 @@ +@extends('admin.layouts.app') + +@section('title', 'SQL 战术沙盒探针') + +@section('content') + +
+

+ ⚠️ 顶级安全警告 +

+

+ 此操作直接连通底层 MySQL 数据库。为杜绝《删库跑路》等生产事故,本控制台已硬编码拦截过滤:只会放行以 SELECT, SHOW, + EXPLAIN 等起手的纯只读语句。所有的增删改一律阻断。 +

+
+ +
+
+
+ @csrf + +
+ + +
+ +
+ +
+
+
+
+ + {{-- 结果展示区 --}} + @isset($error) +
+ {{ $error }} +
+ @endif + + @isset($results) +
+
+ 查询结果 (共 {{ count($results) }} 条) +
+ +
+ @if (empty($results)) +
SQL 执行成功,但返回了空结果集 (0 rows)
+ @else + + + + @foreach ($columns as $col) + + @endforeach + + + + @foreach ($results as $row) + + @foreach ($columns as $col) + + @endforeach + + @endforeach + +
+ {{ $col }}
{{ $row->$col ?? 'NULL' }}
+ @endif +
+
+ @endisset + + + + @endsection diff --git a/resources/views/admin/system/edit.blade.php b/resources/views/admin/system/edit.blade.php new file mode 100644 index 0000000..fc5427c --- /dev/null +++ b/resources/views/admin/system/edit.blade.php @@ -0,0 +1,44 @@ +@extends('admin.layouts.app') + +@section('title', '系统参数配置') + +@section('content') +
+
+
+

修改系统运行参数

+

保存后会同步更新 Redis 缓存,前台实时生效。

+
+
+ +
+
+ @csrf + @method('PUT') + +
+ @foreach ($params as $alias => $body) +
+ + @if (strlen($body) > 50 || str_contains($body, "\n") || str_contains($body, '<')) + + @else + + @endif +
+ @endforeach +
+ +
+ +
+
+
+
+@endsection diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php new file mode 100644 index 0000000..b9e76f3 --- /dev/null +++ b/resources/views/admin/users/index.blade.php @@ -0,0 +1,138 @@ +@extends('admin.layouts.app') + +@section('title', '用户检索与管理') + +@section('content') + +
+
+
+ + + 重置 +
+
+ + +
+ + + + + + + + + + + + + + @foreach ($users as $user) + + + + + + + + + + @endforeach + +
ID注册名性别等级个性签名注册时间管理操作
{{ $user->id }} +
+ + {{ $user->username }} +
+
{{ $user->sex }} + + LV.{{ $user->user_level }} + + + {{ $user->sign ?: '-' }}{{ $user->created_at->format('Y/m/d H:i') }} + + + +
+ @csrf @method('DELETE') + +
+
+
+ + + @if ($users->hasPages()) +
+ {{ $users->links() }} +
+ @endif + + + +
+ +@endsection diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php new file mode 100644 index 0000000..ed1d19d --- /dev/null +++ b/resources/views/chat/frame.blade.php @@ -0,0 +1,488 @@ + + + + + + + {{ $room->name ?? '聊天室' }} - 飘落流星 + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/chat.js']) + + + + + + +
+ +
+
+ {{ $room->name }} + {{ $room->description ?? '欢迎光临!' }} +
+
+ {{ $user->username }} + (LV.{{ $user->user_level }}) + +
+
+ + +
+
-- 以上是历史消息 --
+ +
+ + +
+
+ +
+ + + + + + + +
+ +
+ + + +
+
+
+
+ + + + + +
+ + +
+ + + + + diff --git a/resources/views/guestbook/index.blade.php b/resources/views/guestbook/index.blade.php new file mode 100644 index 0000000..a3d9025 --- /dev/null +++ b/resources/views/guestbook/index.blade.php @@ -0,0 +1,244 @@ +@extends('layouts.app') + +@section('title', '星光留言板') + +@section('content') +
+ + +
+
+
+ +
+ + + + + 返回大厅 + +
+
+ ✉️ +

星光留言板

+
+
+ + + +
+
+
+ + @if (session('success')) + + @endif + @if (session('error')) + + @endif + @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + + +
+
+
+ @csrf +
+
+ + +
+
+ +
+
+ +
+ + +
+ +
+ +
+
+
+
+ + +
+ + + + + +
+
+ + @forelse($messages as $msg) + @php + // 判断是否属于自己发或收的悄悄话,用于高亮 + $isSecret = $msg->secret == 1; + $isToMe = $msg->towho === Auth::user()->username; + $isFromMe = $msg->who === Auth::user()->username; + @endphp + +
+ + + @if ($isSecret) +
+ 🔒 私密信件 +
+ @endif + +
+
+ {{ $msg->who }} + + {{ $msg->towho ?: '大家' }} + 留言: +
+
+ {{ \Carbon\Carbon::parse($msg->post_time)->diffForHumans() }} + + + @if ($isFromMe || $isToMe || Auth::user()->user_level >= 15) +
+ @csrf + @method('DELETE') + +
+ @endif +
+
+ +
+ {!! nl2br(e($msg->text_body)) !!} +
+ + + @if ($msg->who !== Auth::user()->username) +
+ +
+ @endif +
+ @empty +
+ 📭 +

暂无信件

+

这里是空空如也的荒原。

+ +
+ @endforelse + + +
+ {{ $messages->links() }} +
+ +
+
+
+
+ + + + +@endsection diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php new file mode 100644 index 0000000..b5dc7cb --- /dev/null +++ b/resources/views/index.blade.php @@ -0,0 +1,130 @@ + + + + + + {{ \App\Models\SysParam::where('alias', 'sys_name')->value('body') ?? '飘落的流星在线聊天' }} - 登录 + + + + + + +
+

+ {{ \App\Models\SysParam::where('alias', 'sys_name')->value('body') ?? '在线聊天室' }} +

+ +

+ {{ \App\Models\SysParam::where('alias', 'sys_notice')->value('body') ?? '欢迎您的加入' }} +

+ + + + +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+ + 验证码 +
+
+
+ + +
+
+ + + + diff --git a/resources/views/leaderboard/index.blade.php b/resources/views/leaderboard/index.blade.php new file mode 100644 index 0000000..eded664 --- /dev/null +++ b/resources/views/leaderboard/index.blade.php @@ -0,0 +1,131 @@ +@extends('layouts.app') + +@section('title', '风云排行榜') + +@section('content') +
+ + +
+
+
+ +
+ + + + + 返回大厅 + +
+
+ 🏆 +

风云排行榜

+
+
+ + +
+ + +
+
+
+
+ + +
+
+

✨ 数据每 15分钟 + 自动刷新一次。努力提升自己,让全服铭记你的名字!

+
+
+ + +
+
+ +
+ + +
+
+

👑 无上境界榜

+ Level +
+
+ @include('leaderboard.partials.list', [ + 'users' => $topLevels, + 'valueField' => 'user_level', + 'unit' => '级', + 'color' => 'text-red-600', + ]) +
+
+ + +
+
+

🔥 苦修经验榜

+ Exp +
+
+ @include('leaderboard.partials.list', [ + 'users' => $topExp, + 'valueField' => 'exp_num', + 'unit' => '点', + 'color' => 'text-amber-600', + ]) +
+
+ + +
+
+

💰 盖世神豪榜

+ Coin +
+
+ @include('leaderboard.partials.list', [ + 'users' => $topWealth, + 'valueField' => 'jjb', + 'unit' => '万', + 'color' => 'text-yellow-600', + ]) +
+
+ + +
+
+

🌸 绝世名伶榜

+ Charm +
+
+ @include('leaderboard.partials.list', [ + 'users' => $topCharm, + 'valueField' => 'meili', + 'unit' => '点', + 'color' => 'text-pink-600', + ]) +
+
+ +
+ +
+
+
+@endsection diff --git a/resources/views/leaderboard/partials/list.blade.php b/resources/views/leaderboard/partials/list.blade.php new file mode 100644 index 0000000..f68dd27 --- /dev/null +++ b/resources/views/leaderboard/partials/list.blade.php @@ -0,0 +1,57 @@ +
    + @forelse($users as $index => $user) + @php + // 前三名前景色 + $rankBg = 'bg-gray-100 text-gray-500'; + $rowBg = 'hover:bg-gray-50'; + if ($index === 0) { + $rankBg = 'bg-yellow-400 text-yellow-900 shadow-md transform scale-110'; + $rowBg = 'bg-yellow-50 hover:bg-yellow-100 border-l-4 border-yellow-400'; + } elseif ($index === 1) { + $rankBg = 'bg-gray-300 text-gray-800 shadow-sm transform scale-105'; + $rowBg = 'bg-gray-50 hover:bg-gray-100 border-l-4 border-gray-300'; + } elseif ($index === 2) { + $rankBg = 'bg-orange-300 text-orange-900 shadow-sm'; + $rowBg = 'bg-orange-50 hover:bg-orange-100 border-l-4 border-orange-300'; + } + @endphp +
  • + +
    +
    + {{ $index + 1 }} +
    +
    + +
    + + {{ $user->username }} + @if ($user->sex == '女') + + @elseif($user->sex == '男') + + @endif + + {{ $user->sign ?: '这家伙很懒,什么也没留下' }} +
    +
    +
    + + +
    + + {{ number_format($user->$valueField) }} + {{ $unit }} + +
    +
  • + @empty +
  • + 暂无数据登榜 +
  • + @endforelse +
diff --git a/resources/views/rooms/index.blade.php b/resources/views/rooms/index.blade.php new file mode 100644 index 0000000..3949ca5 --- /dev/null +++ b/resources/views/rooms/index.blade.php @@ -0,0 +1,434 @@ + + + + + + + 聊天大厅 - 飘落流星 + + + + + + + + + + + + +
+ + + @if (session('success')) +
+

操作成功

+

{{ session('success') }}

+
+ @endif + @if (session('error')) +
+

发生错误

+

{{ session('error') }}

+
+ @endif + @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + +
+

公开频段 ({{ $rooms->count() }})

+
+ + +
+ @forelse($rooms as $room) +
+ + {{-- 卡片头部 --}} +
+ @if ($room->is_system) + 官方驻地 + @endif +

+ {{ $room->name }}

+

+ {{ $room->description ?: '房主很懒,什么都没写...' }}

+
+ 房主:{{ $room->master }} +
+
+ + {{-- 底部操作区 --}} +
+ + {{-- 管理按钮组(仅房主或超管可见) --}} +
+ @if ($room->master == Auth::user()->username || Auth::user()->user_level >= 15) + + + + @if (!$room->is_system) + + + +
+ @csrf @method('delete') + +
+ @endif + @endif +
+ + {{-- 进入按钮 --}} + + 立刻进入 → + +
+
+ @empty +
+

大厅空空如也

+

目前还没有人创建任何房间。

+
+ @endforelse +
+
+ + + + + + + + + + + + + + + + + + + diff --git a/routes/channels.php b/routes/channels.php index df2ad28..4bf1e3b 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -5,3 +5,19 @@ use Illuminate\Support\Facades\Broadcast; Broadcast::channel('App.Models.User.{id}', function ($user, $id) { return (int) $user->id === (int) $id; }); + +// 聊天室房间 Presence Channel 鉴权与成员信息抓取 +Broadcast::channel('room.{roomId}', function ($user, $roomId) { + // 这里未来可以增加判断:比如该房间是否被锁定,或者该用户是否在此房间的黑名单中 + // 凡是通过了这个判断的人(返回一个数组),他就会成功建立 WebSocket, + // 且他的这个数组信息会被 Reverb 推送给这个房间内的所有其他人 (joining / here 事件)。 + + return [ + 'id' => $user->id, + 'username' => $user->username, + 'user_level' => $user->user_level, + 'sex' => $user->sex, + 'headface' => $user->headface, + // 这里可以视情况加入更多需要前端渲染在线人员列表的字段 + ]; +}); diff --git a/routes/web.php b/routes/web.php index 86a06c5..54f5222 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,77 @@ name('home'); + +// 处理登录/自动注册请求 +Route::post('/login', [AuthController::class, 'login'])->name('login.post'); + +// 处理退出登录 +Route::post('/logout', [AuthController::class, 'logout'])->name('logout'); + +// 聊天室系统内部路由 (需要鉴权) +Route::middleware(['chat.auth'])->group(function () { + // ---- 第六阶段:大厅与房间管理 ---- + Route::get('/rooms', [RoomController::class, 'index'])->name('rooms.index'); + Route::post('/rooms', [RoomController::class, 'store'])->name('rooms.store'); + Route::put('/rooms/{id}', [RoomController::class, 'update'])->name('rooms.update'); + Route::delete('/rooms/{id}', [RoomController::class, 'destroy'])->name('rooms.destroy'); + Route::post('/rooms/{id}/transfer', [RoomController::class, 'transfer'])->name('rooms.transfer'); + + // ---- 第九阶段:外围矩阵 - 风云排行榜 ---- + Route::get('/leaderboard', [\App\Http\Controllers\LeaderboardController::class, 'index'])->name('leaderboard.index'); + + // ---- 第十阶段:站内信与留言板系统 ---- + Route::get('/guestbook', [\App\Http\Controllers\GuestbookController::class, 'index'])->name('guestbook.index'); + Route::post('/guestbook', [\App\Http\Controllers\GuestbookController::class, 'store'])->middleware('throttle:10,1')->name('guestbook.store'); + Route::delete('/guestbook/{id}', [\App\Http\Controllers\GuestbookController::class, 'destroy'])->name('guestbook.destroy'); + + // ---- 第七阶段:用户资料与特权管理 ---- + Route::get('/user/{username}', [UserController::class, 'show'])->name('user.show'); + Route::put('/user/profile', [UserController::class, 'updateProfile'])->name('user.update_profile'); + Route::put('/user/password', [UserController::class, 'changePassword'])->name('user.update_password'); + Route::post('/user/{username}/kick', [UserController::class, 'kick'])->name('user.kick'); + Route::post('/user/{username}/mute', [UserController::class, 'mute'])->name('user.mute'); + + // ---- 第五阶段:具体房间内部聊天核心 ---- + // 进入具体房间界面的初始化 + Route::get('/room/{id}', [ChatController::class, 'init'])->name('chat.room'); + + // 发送消息 + Route::post('/room/{id}/send', [ChatController::class, 'send'])->name('chat.send'); + + // 挂机心跳存点 (限制每分钟最多调用 2 次防止挂机脚本当作 DDOS) + Route::post('/room/{id}/heartbeat', [ChatController::class, 'heartbeat']) + ->middleware('throttle:2,1') + ->name('chat.heartbeat'); + + // 退出房间 + Route::post('/room/{id}/leave', [ChatController::class, 'leave'])->name('chat.leave'); +}); + +// 强力特权层中间件:同时验证 chat.auth 登录态 和 chat.level:15 特权 +Route::middleware(['chat.auth', 'chat.level:15'])->prefix('admin')->name('admin.')->group(function () { + // 后台首页概览 + Route::get('/', [\App\Http\Controllers\Admin\DashboardController::class, 'index'])->name('dashboard'); + + // 系统参数配置 (替代 VIEWSYS.ASP / SetSYS.ASP) + Route::get('/system', [\App\Http\Controllers\Admin\SystemController::class, 'edit'])->name('system.edit'); + Route::put('/system', [\App\Http\Controllers\Admin\SystemController::class, 'update'])->name('system.update'); + + // 用户大盘管理 (替代 gl/ 目录下的各种用户管理功能) + Route::get('/users', [\App\Http\Controllers\Admin\UserManagerController::class, 'index'])->name('users.index'); + Route::put('/users/{id}', [\App\Http\Controllers\Admin\UserManagerController::class, 'update'])->name('users.update'); + Route::delete('/users/{id}', [\App\Http\Controllers\Admin\UserManagerController::class, 'destroy'])->name('users.destroy'); // 物理封杀 + + // 特殊高危操作日志与运维工具 (选做或简易实现 SQL.ASP) + Route::get('/sql', [\App\Http\Controllers\Admin\SqlController::class, 'index'])->name('sql.index'); + Route::post('/sql', [\App\Http\Controllers\Admin\SqlController::class, 'execute'])->name('sql.execute'); // ⚠ 强烈限制为纯 SELECT 查询 });