feat: 实现挂机修仙、排行榜、大厅重构与全站留言板系统
- (Phase 8) 后台各维度管理与配置 - (Phase 9) 全自动静默挂机修仙升级 - (Phase 9) 四大维度风云排行榜页面 - (Phase 10) 全站留言板与悄悄话私信功能 - 运行 Pint 代码格式化
This commit is contained in:
59
app/Events/MessageSent.php
Normal file
59
app/Events/MessageSent.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户发送新消息广播事件
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MessageSent implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
* @param array $message 发送的消息数据
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $roomId,
|
||||
public readonly array $message,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* 聊天消息广播至包含在线状态管理的 PresenceChannel。
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PresenceChannel('room.'.$this->roomId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取广播时的数据
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'message' => $this->message,
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Events/RoomTitleUpdated.php
Normal file
58
app/Events/RoomTitleUpdated.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:房间标题变更广播事件
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class RoomTitleUpdated implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
* @param string $title 新房间标题
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $roomId,
|
||||
public readonly string $title,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PresenceChannel('room.'.$this->roomId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取广播时的数据
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'message' => "系统提示:房间标题已变更为「{$this->title}」",
|
||||
];
|
||||
}
|
||||
}
|
||||
60
app/Events/UserJoined.php
Normal file
60
app/Events/UserJoined.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户进入房间广播事件
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserJoined implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
* @param string $username 进入的用户昵称
|
||||
* @param array $userInfo 用户的属性(如性别,等级等)
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $roomId,
|
||||
public readonly string $username,
|
||||
public readonly array $userInfo,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PresenceChannel('room.'.$this->roomId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取广播时的数据
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->username,
|
||||
'info' => $this->userInfo,
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Events/UserKicked.php
Normal file
58
app/Events/UserKicked.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户被踢出房间广播事件
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserKicked implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
* @param string $username 被踢出的用户昵称
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $roomId,
|
||||
public readonly string $username,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PresenceChannel('room.'.$this->roomId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取广播时的数据
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->username,
|
||||
'message' => "用户 [{$this->username}] 已被踢出聊天室。",
|
||||
];
|
||||
}
|
||||
}
|
||||
57
app/Events/UserLeft.php
Normal file
57
app/Events/UserLeft.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户离开房间广播事件
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserLeft implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
* @param string $username 离开的用户昵称
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $roomId,
|
||||
public readonly string $username,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PresenceChannel('room.'.$this->roomId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取广播时的数据
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'username' => $this->username,
|
||||
];
|
||||
}
|
||||
}
|
||||
33
app/Events/UserLevelUp.php
Normal file
33
app/Events/UserLevelUp.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserLevelUp
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PrivateChannel('channel-name'),
|
||||
];
|
||||
}
|
||||
}
|
||||
65
app/Events/UserMuted.php
Normal file
65
app/Events/UserMuted.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户被封口/解封广播事件
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PresenceChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class UserMuted implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new event instance.
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
* @param string $username 被封口的用户昵称
|
||||
* @param int $muteTime 封口时长(如 10 分钟),如果为 0 则是解封
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly int $roomId,
|
||||
public readonly string $username,
|
||||
public readonly int $muteTime,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the channels the event should broadcast on.
|
||||
*
|
||||
* @return array<int, \Illuminate\Broadcasting\Channel>
|
||||
*/
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PresenceChannel('room.'.$this->roomId),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取广播时的数据
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
$statusMessage = $this->muteTime > 0
|
||||
? "用户 [{$this->username}] 已被系统封口 {$this->muteTime} 次发言时间。"
|
||||
: "用户 [{$this->username}] 已被解除封口。";
|
||||
|
||||
return [
|
||||
'username' => $this->username,
|
||||
'mute_time' => $this->muteTime,
|
||||
'message' => $statusMessage,
|
||||
];
|
||||
}
|
||||
}
|
||||
33
app/Http/Controllers/Admin/DashboardController.php
Normal file
33
app/Http/Controllers/Admin/DashboardController.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台首页控制台
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示后台首页与全局统计
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$stats = [
|
||||
'total_users' => User::count(),
|
||||
'total_rooms' => Room::count(),
|
||||
// 更多统计指标以后再发掘
|
||||
];
|
||||
|
||||
return view('admin.dashboard', compact('stats'));
|
||||
}
|
||||
}
|
||||
76
app/Http/Controllers/Admin/SqlController.php
Normal file
76
app/Http/Controllers/Admin/SqlController.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台 SQL 探针
|
||||
* (替代原版 SQL.ASP,严格限制为只读模式)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SqlController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示 SQL 执行沙盒界面
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('admin.sql.index', ['results' => 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
app/Http/Controllers/Admin/SystemController.php
Normal file
60
app/Http/Controllers/Admin/SystemController.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:系统参数配置控制器
|
||||
* (替代原版 VIEWSYS.ASP / SetSYS.ASP)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SysParam;
|
||||
use App\Services\ChatStateService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SystemController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 显示全局参数配置表单
|
||||
*/
|
||||
public function edit(): View
|
||||
{
|
||||
// 读取数据库中最新的参数
|
||||
$params = SysParam::all()->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', '系统参数已成功更新并生效!');
|
||||
}
|
||||
}
|
||||
114
app/Http/Controllers/Admin/UserManagerController.php
Normal file
114
app/Http/Controllers/Admin/UserManagerController.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台用户大盘管理控制器
|
||||
* (替代原版 gl/ 下的各种管理面)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserManagerController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示拥护列表及搜索
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$query = User::query();
|
||||
|
||||
if ($request->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', '目标已被物理删除。');
|
||||
}
|
||||
}
|
||||
121
app/Http/Controllers/AuthController.php
Normal file
121
app/Http/Controllers/AuthController.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:认证控制器 (处理登录即注册等逻辑)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\LoginRequest;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* 处理用户登录/注册尝试。
|
||||
* 逻辑:
|
||||
* 1. 如果用户已存在,验证密码。为了兼容老数据库,先验证Bcrypt,再退化验证MD5。如果MD5正确则升级为Bcrypt。
|
||||
* 2. 如果用户不存在,直接注册新用户并登录。
|
||||
*/
|
||||
public function login(LoginRequest $request): JsonResponse
|
||||
{
|
||||
$credentials = $request->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' => '已成功退出。']);
|
||||
}
|
||||
}
|
||||
200
app/Http/Controllers/ChatController.php
Normal file
200
app/Http/Controllers/ChatController.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:聊天室核心控制器
|
||||
* 接管原版 INIT.ASP, NEWSAY.ASP, LEAVE.ASP 的所有职责
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\MessageSent;
|
||||
use App\Events\UserJoined;
|
||||
use App\Events\UserLeft;
|
||||
use App\Http\Requests\SendMessageRequest;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\Room;
|
||||
use App\Services\ChatStateService;
|
||||
use App\Services\MessageFilterService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ChatController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ChatStateService $chatState,
|
||||
private readonly MessageFilterService $filter,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 进入房间初始化 (等同于原版 INIT.ASP)
|
||||
*
|
||||
* @param int $id 房间ID
|
||||
* @return View|JsonResponse
|
||||
*/
|
||||
public function init(int $id)
|
||||
{
|
||||
$room = Room::findOrFail($id);
|
||||
$user = Auth::user();
|
||||
|
||||
// 1. 将当前用户加入到 Redis 房间在线列表
|
||||
$this->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']);
|
||||
}
|
||||
}
|
||||
125
app/Http/Controllers/GuestbookController.php
Normal file
125
app/Http/Controllers/GuestbookController.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:全站留言板与站内悄悄信控制器
|
||||
* (替代原版 Guestbook 系列功能)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreGuestbookRequest;
|
||||
use App\Models\Guestbook;
|
||||
use App\Models\User;
|
||||
use App\Services\MessageFilterService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GuestbookController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MessageFilterService $filter
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 留言簿主面板 (支持分类 Tab: public/inbox/outbox)
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$tab = $request->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', '该行留言已被抹除。');
|
||||
}
|
||||
}
|
||||
71
app/Http/Controllers/LeaderboardController.php
Normal file
71
app/Http/Controllers/LeaderboardController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:全局风云排行榜控制器
|
||||
* 各种维度(等级、经验、交友币、魅力)的前20名抓取与缓存展示。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class LeaderboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* 渲染排行榜主视角
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
// 缓存 15 分钟,防止每秒几百个人看排行榜把数据库扫死
|
||||
// 选用 remember 则在过期时自动执行闭包查询并重置缓存
|
||||
$ttl = 60 * 15;
|
||||
|
||||
// 1. 境界榜 (以 user_level 为尊)
|
||||
$topLevels = Cache::remember('leaderboard:top_levels', $ttl, function () {
|
||||
return User::select('id', 'username', 'headface', 'user_level', 'sex', 'sign')
|
||||
->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'));
|
||||
}
|
||||
}
|
||||
129
app/Http/Controllers/RoomController.php
Normal file
129
app/Http/Controllers/RoomController.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:房间管理器
|
||||
* 接管原版 ROOMLIST.ASP, NEWROOM.ASP, ROOMSET.ASP, CUTROOM.ASP, OVERROOM.ASP 的职责
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\RoomTitleUpdated;
|
||||
use App\Http\Requests\StoreRoomRequest;
|
||||
use App\Http\Requests\UpdateRoomRequest;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RoomController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示房间大厅列表 (对应 ROOMLIST.ASP)
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$rooms = Room::with('masterUser')
|
||||
->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}]。");
|
||||
}
|
||||
}
|
||||
149
app/Http/Controllers/UserController.php
Normal file
149
app/Http/Controllers/UserController.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户中心与管理控制器
|
||||
* 接管原版 USERinfo.ASP, USERSET.ASP, chpasswd.asp, KILLUSER.ASP
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Events\UserKicked;
|
||||
use App\Events\UserMuted;
|
||||
use App\Http\Requests\ChangePasswordRequest;
|
||||
use App\Http\Requests\UpdateProfileRequest;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
/**
|
||||
* 查看其他用户资料片 (对应 USERinfo.ASP)
|
||||
*/
|
||||
public function show(string $username): JsonResponse
|
||||
{
|
||||
$user = User::where('username', $username)->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} 分钟。"]);
|
||||
}
|
||||
}
|
||||
39
app/Http/Middleware/ChatAuthenticate.php
Normal file
39
app/Http/Middleware/ChatAuthenticate.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:聊天室登录验证中间件
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ChatAuthenticate
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['message' => '未登录', 'status' => 'error'], 401);
|
||||
}
|
||||
|
||||
return redirect()->route('home')->withErrors([
|
||||
'auth' => '请先输入昵称进入聊天室',
|
||||
]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
45
app/Http/Middleware/LevelRequired.php
Normal file
45
app/Http/Middleware/LevelRequired.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户等级权限验证中间件
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class LevelRequired
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
* 校验当前登录用户的等级是否大于或等于要求等级。
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
* @param int $level 要求达到的最低等级 (例如 15 为室主)
|
||||
*/
|
||||
public function handle(Request $request, Closure $next, int $level): Response
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return redirect()->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);
|
||||
}
|
||||
}
|
||||
47
app/Http/Requests/ChangePasswordRequest.php
Normal file
47
app/Http/Requests/ChangePasswordRequest.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户修改密码请求验证器
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class ChangePasswordRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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' => '两次输入的新密码不一致。',
|
||||
];
|
||||
}
|
||||
}
|
||||
64
app/Http/Requests/LoginRequest.php
Normal file
64
app/Http/Requests/LoginRequest.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:登录/注册请求验证器
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LoginRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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<string, string>
|
||||
*/
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'username.required' => '必须填写用户名。',
|
||||
'username.min' => '用户名长度不得少于 2 个字符。',
|
||||
'username.max' => '用户名长度不得超过 12 个字符。',
|
||||
'username.regex' => '用户名包含非法字符(不允许使用尖括号或引号)。',
|
||||
'password.required' => '必须填写密码。',
|
||||
'password.min' => '密码长度不得少于 1 个字符。',
|
||||
'captcha.required' => '必须填写验证码。',
|
||||
'captcha.captcha' => '验证码不正确。',
|
||||
];
|
||||
}
|
||||
}
|
||||
48
app/Http/Requests/SendMessageRequest.php
Normal file
48
app/Http/Requests/SendMessageRequest.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:聊天室发言请求验证器
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SendMessageRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true; // 权限验证已交由中间件处理
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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 个字符。',
|
||||
];
|
||||
}
|
||||
}
|
||||
39
app/Http/Requests/StoreGuestbookRequest.php
Normal file
39
app/Http/Requests/StoreGuestbookRequest.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreGuestbookRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return \Illuminate\Support\Facades\Auth::check(); // 必须登录
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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 个字符。',
|
||||
];
|
||||
}
|
||||
}
|
||||
49
app/Http/Requests/StoreRoomRequest.php
Normal file
49
app/Http/Requests/StoreRoomRequest.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:新建聊天室请求验证器
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class StoreRoomRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
// 只有登录用户,且 user_level 达到特定阈值(例如 >= 10)才可以自己建房
|
||||
// 具体阈值可以根据运营需求调整,此处暂设 10 为门槛。
|
||||
return Auth::check() && Auth::user()->user_level >= 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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 个字符。',
|
||||
];
|
||||
}
|
||||
}
|
||||
46
app/Http/Requests/UpdateProfileRequest.php
Normal file
46
app/Http/Requests/UpdateProfileRequest.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户修改个人资料请求验证器
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateProfileRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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' => '必须选择一个头像。',
|
||||
];
|
||||
}
|
||||
}
|
||||
46
app/Http/Requests/UpdateRoomRequest.php
Normal file
46
app/Http/Requests/UpdateRoomRequest.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:修改聊天室设置请求验证器
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateRoomRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
// 权限判断(是否为房主)会直接在 Controller/Policy 中进行
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|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' => '该房间名称已存在。',
|
||||
];
|
||||
}
|
||||
}
|
||||
50
app/Jobs/SaveMessageJob.php
Normal file
50
app/Jobs/SaveMessageJob.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:异步持久化聊天记录队列
|
||||
* 承接高频聊天缓存,防堵塞 MySQL 进程。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Message;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Queue\Queueable;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class SaveMessageJob implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param array $messageData 包装好的消息数组
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $messageData
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
* 将缓存在 Redis 刚广播出去的消息,真实映射写入到 `messages` 数据表。
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
Message::create([
|
||||
'room_id' => $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']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
31
app/Models/Action.php
Normal file
31
app/Models/Action.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:动作表情模型
|
||||
*
|
||||
* 对应原 ASP 文件:action 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Action extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'act_name',
|
||||
'alias',
|
||||
'toall',
|
||||
'toself',
|
||||
'toother',
|
||||
];
|
||||
}
|
||||
60
app/Models/AdminLog.php
Normal file
60
app/Models/AdminLog.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:管理员操作统计模型
|
||||
*
|
||||
* 对应原 ASP 文件:admincz 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AdminLog extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
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<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'in_time' => 'datetime',
|
||||
'jl_time' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Models/AuditLog.php
Normal file
41
app/Models/AuditLog.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:操作日志模型
|
||||
*
|
||||
* 对应原 ASP 文件:record 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AuditLog extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'occ_time',
|
||||
'occ_env',
|
||||
'stype',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'occ_time' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
44
app/Models/FriendCall.php
Normal file
44
app/Models/FriendCall.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:好友呼叫模型
|
||||
*
|
||||
* 对应原 ASP 文件:calls 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FriendCall extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'who',
|
||||
'towho',
|
||||
'callmess',
|
||||
'calltime',
|
||||
'read',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'calltime' => 'datetime',
|
||||
'read' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Models/FriendRequest.php
Normal file
41
app/Models/FriendRequest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:好友申请模型
|
||||
*
|
||||
* 对应原 ASP 文件:friendrq 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class FriendRequest extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'who',
|
||||
'towho',
|
||||
'sub_time',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'sub_time' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
49
app/Models/Guestbook.php
Normal file
49
app/Models/Guestbook.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:留言板模型
|
||||
*
|
||||
* 对应原 ASP 文件:guestbook 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Guestbook extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'who',
|
||||
'towho',
|
||||
'secret',
|
||||
'ip',
|
||||
'email',
|
||||
'web',
|
||||
'addr',
|
||||
'post_time',
|
||||
'text_title',
|
||||
'text_body',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'post_time' => 'datetime',
|
||||
'secret' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Models/IpLock.php
Normal file
41
app/Models/IpLock.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:IP 封禁模型
|
||||
*
|
||||
* 对应原 ASP 文件:ip_lock 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class IpLock extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'ip',
|
||||
'end_time',
|
||||
'act_level',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'end_time' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
41
app/Models/IpLog.php
Normal file
41
app/Models/IpLog.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:IP 登录日志模型
|
||||
*
|
||||
* 对应原 ASP 文件:ip 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class IpLog extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'ip',
|
||||
'sdate',
|
||||
'uuname',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'sdate' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
44
app/Models/Marriage.php
Normal file
44
app/Models/Marriage.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:婚姻关系模型
|
||||
*
|
||||
* 对应原 ASP 文件:hy / lh 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Marriage extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'hyname',
|
||||
'hyname1',
|
||||
'hytime',
|
||||
'hygb',
|
||||
'hyjb',
|
||||
'i',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'hytime' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
47
app/Models/Message.php
Normal file
47
app/Models/Message.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:聊天消息模型
|
||||
*
|
||||
* 对应原 ASP 文件:内存 Application("_says") 变量 (用数据库做持久化归档)
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Message extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
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<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'sent_at' => 'datetime',
|
||||
'is_secret' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
57
app/Models/Room.php
Normal file
57
app/Models/Room.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:聊天房间模型
|
||||
*
|
||||
* 对应原 ASP 文件:room 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Room extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
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<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'room_time' => 'datetime',
|
||||
'build_time' => 'datetime',
|
||||
'room_keep' => 'boolean',
|
||||
'room_tt' => 'boolean',
|
||||
'room_html' => 'boolean',
|
||||
'door_open' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Models/RoomDescription.php
Normal file
28
app/Models/RoomDescription.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:房间描述模板模型
|
||||
*
|
||||
* 对应原 ASP 文件:room_des 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class RoomDescription extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'alias',
|
||||
'describ',
|
||||
];
|
||||
}
|
||||
41
app/Models/ScrollAd.php
Normal file
41
app/Models/ScrollAd.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:滚动广告/公告模型
|
||||
*
|
||||
* 对应原 ASP 文件:scrollad 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ScrollAd extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'ad_title',
|
||||
'ad_link',
|
||||
'ad_new_flag',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'ad_new_flag' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
29
app/Models/SysParam.php
Normal file
29
app/Models/SysParam.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:系统参数模型
|
||||
*
|
||||
* 对应原 ASP 文件:sysparam 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SysParam extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'alias',
|
||||
'guidetxt',
|
||||
'body',
|
||||
];
|
||||
}
|
||||
@@ -1,36 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:主用户模型
|
||||
*
|
||||
* 对应原 ASP 文件:user 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
* @var array<int, string>
|
||||
*/
|
||||
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<string>
|
||||
* @var array<int, string>
|
||||
*/
|
||||
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',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
43
app/Models/UserItem.php
Normal file
43
app/Models/UserItem.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户道具模型
|
||||
*
|
||||
* 对应原 ASP 文件:gg 表
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserItem extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'times',
|
||||
'gg',
|
||||
'dayy',
|
||||
'lx',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'times' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
165
app/Services/ChatStateService.php
Normal file
165
app/Services/ChatStateService.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:聊天全局状态中心 (替代旧版 ASP中的 Application 全局对象)
|
||||
* 依赖 Redis 内存存取实现在线人员列表、发言记录的高并发读写。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\SysParam;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class ChatStateService
|
||||
{
|
||||
/**
|
||||
* 将用户加入到指定房间的在线列表中。
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
* @param string $username 用户名
|
||||
* @param array $info 用户详细信息 (头像、等级等)
|
||||
*/
|
||||
public function userJoin(int $roomId, string $username, array $info): void
|
||||
{
|
||||
$key = "room:{$roomId}:users";
|
||||
Redis::hset($key, $username, json_encode($info, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将用户从指定房间的在线列表中移除。
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
* @param string $username 用户名
|
||||
*/
|
||||
public function userLeave(int $roomId, string $username): void
|
||||
{
|
||||
$key = "room:{$roomId}:users";
|
||||
Redis::hdel($key, $username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定房间的所有在线用户列表。
|
||||
*
|
||||
* @param int $roomId 房间ID
|
||||
*/
|
||||
public function getRoomUsers(int $roomId): array
|
||||
{
|
||||
$key = "room:{$roomId}:users";
|
||||
$users = Redis::hgetall($key);
|
||||
|
||||
$result = [];
|
||||
foreach ($users as $username => $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);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
app/Services/MessageFilterService.php
Normal file
52
app/Services/MessageFilterService.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:聊天内容过滤器 (敏感词与 HTML 净化)
|
||||
* 替代旧版 ASP中的 TrStr() / SHTM() 等各种过滤函数。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class MessageFilterService
|
||||
{
|
||||
/**
|
||||
* 简单的关键词黑名单,未来可放到数据库或 Redis 动态加载
|
||||
*/
|
||||
private array $badWords = [
|
||||
'外挂', '刷单', '脚本', // 示例黑名单
|
||||
];
|
||||
|
||||
/**
|
||||
* 执行过滤净化,保障入库和显示安全。
|
||||
*
|
||||
* @param string $content 原始用户发送内容
|
||||
* @return string 净化后的内容
|
||||
*/
|
||||
public function filter(string $content): string
|
||||
{
|
||||
if (empty($content)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 1. HTML 标签全量脱除,阻绝任意 XSS/HTML 注入
|
||||
$content = strip_tags($content);
|
||||
|
||||
// 2. 敏感词替换
|
||||
foreach ($this->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);
|
||||
}
|
||||
}
|
||||
47
app/Services/UserLevelService.php
Normal file
47
app/Services/UserLevelService.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:用户等级与权限辅助获取服务
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class UserLevelService
|
||||
{
|
||||
/**
|
||||
* 安全地获取用户的当前权限等级。
|
||||
* 结合 Redis 缓存,防止每次判断权限都产生额外的 MySQL 查询。
|
||||
*
|
||||
* @param string $username 目标查询的用户名
|
||||
* @return int 用户等级,如果没有查到默认为 1
|
||||
*/
|
||||
public function getUserLevel(string $username): int
|
||||
{
|
||||
if (empty($username)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (int) Cache::remember("user_level:{$username}", 60, function () use ($username) {
|
||||
$level = User::where('username', $username)->value('user_level');
|
||||
|
||||
return $level !== null ? $level : 1;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 当用户等级被管理员修改时,调用此方法清空其对应缓存。
|
||||
*
|
||||
* @param string $username 用户名
|
||||
*/
|
||||
public function forgetUserLevel(string $username): void
|
||||
{
|
||||
Cache::forget("user_level:{$username}");
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
//
|
||||
|
||||
@@ -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": {
|
||||
|
||||
219
composer.lock
generated
219
composer.lock
generated
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
42
database/migrations/2026_02_26_040510_create_rooms_table.php
Normal file
42
database/migrations/2026_02_26_040510_create_rooms_table.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('rooms', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('audit_logs', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('guestbooks', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ip_locks', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('messages', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('sys_params', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('actions', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('admin_logs', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('friend_calls', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('friend_requests', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ip_logs', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('marriages', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('room_descriptions', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('scroll_ads', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_items', function (Blueprint $table) {
|
||||
$table->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');
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
31
database/seeders/SysParamSeeder.php
Normal file
31
database/seeders/SysParamSeeder.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\SysParam;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class SysParamSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$params = [
|
||||
['alias' => '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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
19
resources/js/bootstrap.js
vendored
19
resources/js/bootstrap.js
vendored
@@ -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"],
|
||||
});
|
||||
|
||||
63
resources/js/chat.js
Normal file
63
resources/js/chat.js
Normal file
@@ -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;
|
||||
47
resources/views/admin/dashboard.blade.php
Normal file
47
resources/views/admin/dashboard.blade.php
Normal file
@@ -0,0 +1,47 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', '仪表盘')
|
||||
|
||||
@section('content')
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<h3 class="text-gray-500 text-sm font-medium mb-1">总计注册用户数</h3>
|
||||
<p class="text-3xl font-bold text-gray-800">{{ $stats['total_users'] }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm p-6 border border-gray-100">
|
||||
<h3 class="text-gray-500 text-sm font-medium mb-1">总计聊天频道数</h3>
|
||||
<p class="text-3xl font-bold text-gray-800">{{ $stats['total_rooms'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
||||
<h2 class="text-lg font-bold text-gray-800">系统信息摘要</h2>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<ul class="space-y-3">
|
||||
<li class="flex items-center font-mono text-sm">
|
||||
<span class="w-32 text-gray-500 inline-block font-sans">Laravel 版本:</span>
|
||||
<span class="text-indigo-600">{{ app()->version() }}</span>
|
||||
</li>
|
||||
<li class="flex items-center font-mono text-sm">
|
||||
<span class="w-32 text-gray-500 inline-block font-sans">PHP 版本:</span>
|
||||
<span class="text-indigo-600">{{ PHP_VERSION }}</span>
|
||||
</li>
|
||||
<li class="flex items-center text-sm font-mono mt-4 pt-4 border-t">
|
||||
<span class="mr-4 text-gray-500 inline-block font-sans items-center flex">队列监控面板</span>
|
||||
<!-- Laravel Horizon 的默认路由前缀由开发者确认或自己改。这里默认是 /horizon -->
|
||||
<a href="{{ url('/horizon') }}" target="_blank"
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline flex items-center">
|
||||
<span>打开 Horizon 控制台</span>
|
||||
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
83
resources/views/admin/layouts/app.blade.php
Normal file
83
resources/views/admin/layouts/app.blade.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>后台管理 - 飘落流星</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 flex h-screen text-gray-800">
|
||||
<!-- 左侧侧边栏 -->
|
||||
<aside class="w-64 bg-slate-900 text-white flex flex-col">
|
||||
<div class="p-6 text-center border-b border-white/10">
|
||||
<h2 class="text-2xl font-extrabold tracking-widest uppercase">Admin</h2>
|
||||
<p class="text-xs text-slate-400 mt-2">飘落流星 控制台</p>
|
||||
</div>
|
||||
<nav class="flex-1 px-4 py-6 space-y-2 overflow-y-auto">
|
||||
<a href="{{ route('admin.dashboard') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.dashboard') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
📊 仪表盘
|
||||
</a>
|
||||
<a href="{{ route('admin.system.edit') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.system.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
⚙️ 系统参数参数
|
||||
</a>
|
||||
<a href="{{ route('admin.users.index') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.users.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
👥 用户管理
|
||||
</a>
|
||||
<a href="{{ route('admin.sql.index') }}"
|
||||
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.sql.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
|
||||
💾 SQL 探针
|
||||
</a>
|
||||
</nav>
|
||||
<div class="p-4 border-t border-white/10">
|
||||
<a href="{{ route('rooms.index') }}"
|
||||
class="block w-full text-center px-4 py-2 bg-slate-800 hover:bg-slate-700 rounded transition text-sm">
|
||||
返回前台大厅
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 右侧主体内容 -->
|
||||
<main class="flex-1 flex flex-col h-full overflow-hidden relative">
|
||||
<!-- 顶栏 -->
|
||||
<header class="bg-white shadow relative z-20 flex items-center justify-between px-6 py-4">
|
||||
<h1 class="text-xl font-bold text-gray-700">@yield('title', '控制台')</h1>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm font-medium">当前操作人: <span
|
||||
class="text-indigo-600">{{ Auth::user()->username }}</span></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 内容滚动区 -->
|
||||
<div class="flex-1 overflow-y-auto p-6 relative z-10">
|
||||
@if (session('success'))
|
||||
<div class="mb-6 bg-emerald-100 border-l-4 border-emerald-500 text-emerald-700 p-4 rounded shadow-sm">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="mb-6 bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-sm">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="mb-6 bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-sm">
|
||||
<ul class="list-disc list-inside text-sm">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@yield('content')
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
98
resources/views/admin/sql/index.blade.php
Normal file
98
resources/views/admin/sql/index.blade.php
Normal file
@@ -0,0 +1,98 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', 'SQL 战术沙盒探针')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="mb-6 bg-red-50 border-l-4 border-red-500 p-4 rounded text-red-700 shadow-sm text-sm">
|
||||
<p class="font-bold flex items-center">
|
||||
<span class="mr-2">⚠️</span> 顶级安全警告
|
||||
</p>
|
||||
<p class="mt-1 ml-6">
|
||||
此操作直接连通底层 MySQL 数据库。为杜绝《删库跑路》等生产事故,本控制台已硬编码拦截过滤:只会放行以 <code>SELECT</code>, <code>SHOW</code>,
|
||||
<code>EXPLAIN</code> 等起手的<strong>纯只读语句</strong>。所有的增删改一律阻断。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6">
|
||||
<div class="p-6">
|
||||
<form action="{{ route('admin.sql.execute') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">输入原始只读 SQL 语句</label>
|
||||
<textarea name="query" rows="5" required placeholder="SELECT * FROM users ORDER BY id DESC LIMIT 10;"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-4 bg-gray-50 border font-mono resize-y">{{ old('query', $query ?? '') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-slate-800 text-white rounded-md font-bold hover:bg-slate-900 shadow-sm transition flex items-center">
|
||||
<span>🔥 探 针 发 射</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 结果展示区 --}}
|
||||
@isset($error)
|
||||
<div
|
||||
class="bg-red-50 border border-red-200 text-red-700 p-6 rounded-xl shadow-sm mb-6 overflow-x-auto font-mono text-sm whitespace-pre-wrap">
|
||||
{{ $error }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@isset($results)
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="bg-gray-50 px-6 py-3 border-b flex justify-between items-center text-sm font-bold text-gray-700">
|
||||
<span>查询结果 (共 {{ count($results) }} 条)</span>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto p-4 max-h-[600px] custom-scrollbar overflow-y-auto">
|
||||
@if (empty($results))
|
||||
<div class="text-center text-gray-400 py-10 font-bold">SQL 执行成功,但返回了空结果集 (0 rows)</div>
|
||||
@else
|
||||
<table class="w-full text-left border-collapse text-sm">
|
||||
<thead>
|
||||
<tr class="border-b-2 border-indigo-500">
|
||||
@foreach ($columns as $col)
|
||||
<th
|
||||
class="p-3 font-bold text-gray-600 whitespace-nowrap bg-indigo-50/50 sticky top-0 z-10 shadow-sm">
|
||||
{{ $col }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 font-mono">
|
||||
@foreach ($results as $row)
|
||||
<tr class="hover:bg-amber-50 transition">
|
||||
@foreach ($columns as $col)
|
||||
<td class="p-3 whitespace-nowrap text-gray-700">{{ $row->$col ?? 'NULL' }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endisset
|
||||
|
||||
<style>
|
||||
/* 针对该表格页加深一点滚动条以便查看超长字段 */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #94a3b8;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background-color: #f1f5f9;
|
||||
}
|
||||
</style>
|
||||
|
||||
@endsection
|
||||
44
resources/views/admin/system/edit.blade.php
Normal file
44
resources/views/admin/system/edit.blade.php
Normal file
@@ -0,0 +1,44 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', '系统参数配置')
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50">
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-gray-800">修改系统运行参数</h2>
|
||||
<p class="text-xs text-gray-500 mt-1">保存后会同步更新 Redis 缓存,前台实时生效。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<form action="{{ route('admin.system.update') }}" method="POST">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
|
||||
<div class="space-y-6 max-w-2xl">
|
||||
@foreach ($params as $alias => $body)
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">
|
||||
{{ $descriptions[$alias] ?? $alias }}
|
||||
<span class="text-gray-400 font-normal ml-2">[{{ $alias }}]</span>
|
||||
</label>
|
||||
@if (strlen($body) > 50 || str_contains($body, "\n") || str_contains($body, '<'))
|
||||
<textarea name="{{ $alias }}" rows="4"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border whitespace-pre-wrap">{{ $body }}</textarea>
|
||||
@else
|
||||
<input type="text" name="{{ $alias }}" value="{{ $body }}"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border">
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="mt-8 pt-6 border-t flex space-x-3">
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-indigo-600 text-white rounded-md font-bold hover:bg-indigo-700 shadow-sm transition">保存并发布</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
138
resources/views/admin/users/index.blade.php
Normal file
138
resources/views/admin/users/index.blade.php
Normal file
@@ -0,0 +1,138 @@
|
||||
@extends('admin.layouts.app')
|
||||
|
||||
@section('title', '用户检索与管理')
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden mb-6" x-data="{ showEditModal: false, editingUser: {} }">
|
||||
<div class="p-6 border-b border-gray-100 bg-gray-50 flex items-center justify-between">
|
||||
<form action="{{ route('admin.users.index') }}" method="GET" class="flex gap-2">
|
||||
<input type="text" name="username" value="{{ request('username') }}" placeholder="搜索用户名..."
|
||||
class="px-3 py-1.5 border border-gray-300 rounded shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white px-4 py-1.5 rounded hover:bg-indigo-700 font-bold shadow-sm transition">搜索</button>
|
||||
<a href="{{ route('admin.users.index') }}"
|
||||
class="px-4 py-1.5 bg-white border border-gray-300 rounded text-gray-700 hover:bg-gray-50">重置</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr
|
||||
class="bg-gray-50 border-b border-gray-100 text-xs text-gray-500 uppercase font-bold tracking-wider">
|
||||
<th class="p-4">ID</th>
|
||||
<th class="p-4">注册名</th>
|
||||
<th class="p-4">性别</th>
|
||||
<th class="p-4">等级</th>
|
||||
<th class="p-4">个性签名</th>
|
||||
<th class="p-4">注册时间</th>
|
||||
<th class="p-4 text-right">管理操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100">
|
||||
@foreach ($users as $user)
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="p-4 font-mono text-xs text-gray-500">{{ $user->id }}</td>
|
||||
<td class="p-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<img src="/images/headface/{{ $user->headface ?? '01.gif' }}"
|
||||
class="w-8 h-8 rounded border object-cover">
|
||||
<span class="font-bold text-gray-800">{{ $user->username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-4 text-sm">{{ $user->sex }}</td>
|
||||
<td class="p-4">
|
||||
<span
|
||||
class="px-2 py-0.5 rounded-full text-xs {{ $user->user_level >= 15 ? 'bg-red-100 text-red-700 font-bold' : 'bg-gray-100 text-gray-600' }}">
|
||||
LV.{{ $user->user_level }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="p-4 text-sm text-gray-500 truncate max-w-[200px]" title="{{ $user->sign }}">
|
||||
{{ $user->sign ?: '-' }}</td>
|
||||
<td class="p-4 text-sm font-mono text-gray-500">{{ $user->created_at->format('Y/m/d H:i') }}
|
||||
</td>
|
||||
<td class="p-4 text-right space-x-2 relative" x-data>
|
||||
<button
|
||||
@click="editingUser = { id: {{ $user->id }}, username: '{{ addslashes($user->username) }}', user_level: {{ $user->user_level }}, sex: '{{ $user->sex }}', requestUrl: '{{ route('admin.users.update', $user->id) }}' }; showEditModal = true"
|
||||
class="text-xs bg-indigo-50 text-indigo-600 font-bold px-3 py-1.5 rounded hover:bg-indigo-600 hover:text-white transition cursor-pointer">
|
||||
详细 / 修改
|
||||
</button>
|
||||
|
||||
<form action="{{ route('admin.users.destroy', $user->id) }}" method="POST" class="inline"
|
||||
onsubmit="return confirm('危险:确定彻底物理清除用户 [{{ $user->username }}] 吗?数据不可恢复!')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit"
|
||||
class="text-xs bg-red-50 text-red-600 font-bold px-3 py-1.5 rounded hover:bg-red-600 hover:text-white transition cursor-pointer">
|
||||
强杀
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页链接 -->
|
||||
@if ($users->hasPages())
|
||||
<div class="p-4 border-t border-gray-100">
|
||||
{{ $users->links() }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 弹出的修改框 -->
|
||||
<div x-show="showEditModal" style="display: none;"
|
||||
class="fixed inset-0 z-50 bg-black/60 flex items-center justify-center p-4">
|
||||
<div @click.away="showEditModal = false"
|
||||
class="bg-white rounded-xl shadow-2xl w-full max-w-md transform transition-all" x-transition>
|
||||
<div
|
||||
class="bg-indigo-900 border-b border-indigo-800 px-6 py-4 flex justify-between items-center rounded-t-xl text-white">
|
||||
<h3 class="font-bold text-lg">全量修改:<span x-text="editingUser.username" class="text-indigo-300"></span>
|
||||
</h3>
|
||||
<button @click="showEditModal = false" class="text-gray-400 hover:text-white">×</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- 依靠 Alpine 绑定的 AJAX 或者 Form 提交 -->
|
||||
<form :action="editingUser.requestUrl" method="POST" id="adminUserUpdateForm">
|
||||
@csrf @method('PUT')
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">安全等级 (0-99)</label>
|
||||
<input type="number" name="user_level" x-model="editingUser.user_level" required
|
||||
class="w-full border-gray-300 rounded shadow-sm focus:ring-indigo-500">
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">性别</label>
|
||||
<select name="sex" x-model="editingUser.sex"
|
||||
class="w-full border-gray-300 rounded shadow-sm focus:ring-indigo-500">
|
||||
<option value="男">男</option>
|
||||
<option value="女">女</option>
|
||||
<option value="保密">保密</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label
|
||||
class="block text-sm font-bold pl-1 text-red-600 border-l-4 border-red-500 bg-red-50 p-2 mb-2">强制重算密码
|
||||
(留空则不修改)</label>
|
||||
<input type="text" name="password" placeholder="强行输入新密码覆盖"
|
||||
class="w-full border-red-300 rounded shadow-sm focus:border-red-500 focus:ring-red-500 placeholder-red-300">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4 border-t border-gray-100">
|
||||
<button type="button" @click="showEditModal = false"
|
||||
class="px-4 py-2 border rounded font-medium text-gray-600 hover:bg-gray-50">取消</button>
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded font-bold hover:bg-indigo-700 shadow-sm">提交强制改写</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
488
resources/views/chat/frame.blade.php
Normal file
488
resources/views/chat/frame.blade.php
Normal file
@@ -0,0 +1,488 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ $room->name ?? '聊天室' }} - 飘落流星</title>
|
||||
<!-- 引入全局 CSRF Token -->
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<!-- 传递必要的上下文数据给 chat.js 模块使用 -->
|
||||
<script>
|
||||
window.chatContext = {
|
||||
roomId: {{ $room->id }},
|
||||
username: "{{ $user->username }}",
|
||||
userLevel: {{ $user->user_level }},
|
||||
sendUrl: "{{ route('chat.send', $room->id) }}",
|
||||
leaveUrl: "{{ route('chat.leave', $room->id) }}"
|
||||
};
|
||||
</script>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js', 'resources/js/chat.js'])
|
||||
<style>
|
||||
/* 自定义滚动条样式,让界面更清爽 */
|
||||
.chat-scroll::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-scroll::-webkit-scrollbar-thumb {
|
||||
background-color: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chat-scroll::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 flex h-screen overflow-hidden text-sm">
|
||||
|
||||
<!-- 左侧/中间部分:主聊天区域与控制台 -->
|
||||
<div class="flex-1 flex flex-col h-full bg-white relative">
|
||||
<!-- 头部房间信息栏 -->
|
||||
<header
|
||||
class="h-14 border-b bg-gradient-to-r from-blue-500 to-indigo-600 text-white flex items-center justify-between px-6 shadow-sm z-10">
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="font-bold text-lg tracking-wide">{{ $room->name }}</span>
|
||||
<span id="room-title-display"
|
||||
class="text-xs bg-white/20 px-2 py-1 rounded-full">{{ $room->description ?? '欢迎光临!' }}</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm opacity-90"><i class="font-semibold">{{ $user->username }}</i>
|
||||
(LV.{{ $user->user_level }})</span>
|
||||
<button type="button" onclick="leaveRoom()"
|
||||
class="px-3 py-1.5 bg-white/10 hover:bg-red-500 hover:text-white rounded transition text-xs font-semibold">
|
||||
退出房间
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 聊天记录展示区 -->
|
||||
<main id="chat-messages-container" class="flex-1 overflow-y-auto p-4 chat-scroll bg-gray-50/50 space-y-3">
|
||||
<div class="text-center text-xs text-gray-400 my-4">-- 以上是历史消息 --</div>
|
||||
<!-- 气泡动态挂载点 -->
|
||||
</main>
|
||||
|
||||
<!-- 底部发言控制区 -->
|
||||
<footer class="h-auto min-h-24 bg-white border-t p-3 shadow-inner z-10">
|
||||
<form id="chat-form" onsubmit="sendMessage(event)" class="max-w-5xl mx-auto flex flex-col space-y-2">
|
||||
|
||||
<div class="flex items-center space-x-3 text-xs text-gray-600 px-1">
|
||||
<label class="flex items-center space-x-1 cursor-pointer">
|
||||
<span>对</span>
|
||||
<select id="to_user" name="to_user"
|
||||
class="border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-gray-50">
|
||||
<option value="大家" selected>所有人</option>
|
||||
<!-- 在线人员将动态加载到这里 -->
|
||||
</select>
|
||||
<span>说:</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center space-x-1 cursor-pointer">
|
||||
<span>动作:</span>
|
||||
<select id="action" name="action"
|
||||
class="border border-gray-300 rounded px-1 py-0.5 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 bg-gray-50">
|
||||
<option value="">(无动作)</option>
|
||||
<option value="微笑">微笑</option>
|
||||
<option value="大笑">大笑</option>
|
||||
<option value="愤怒">愤怒</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center space-x-1 cursor-pointer">
|
||||
<span>字体色:</span>
|
||||
<input type="color" id="font_color" name="font_color" value="#000000"
|
||||
class="w-6 h-6 p-0 border-0 cursor-pointer rounded">
|
||||
</label>
|
||||
|
||||
<label class="flex items-center space-x-1 cursor-pointer hover:text-blue-600 transition">
|
||||
<input type="checkbox" id="is_secret" name="is_secret" value="1"
|
||||
class="text-blue-500 rounded focus:ring-blue-500">
|
||||
<span>悄悄话</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-end space-x-2">
|
||||
<textarea id="content" name="content" rows="2"
|
||||
class="flex-1 border border-gray-300 rounded-md p-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none resize-none transition-shadow text-sm"
|
||||
placeholder="在这里输入聊天内容,按 Ctrl+Enter 快捷发送..."></textarea>
|
||||
|
||||
<button type="submit" id="send-btn"
|
||||
class="h-11 px-6 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-md shadow transition flex-shrink-0">
|
||||
发送消息
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:在线人员面板 -->
|
||||
<aside class="w-64 bg-gray-50 border-l flex flex-col h-full shadow-lg z-20">
|
||||
<div class="h-14 border-b bg-gray-100 flex items-center justify-center font-bold text-gray-700 tracking-wider">
|
||||
📶 在线人员 (<span id="online-count">0</span>)
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-2 chat-scroll">
|
||||
<ul id="online-users-list" class="space-y-1">
|
||||
<!-- 在线列表渲染点 -->
|
||||
<li class="flex items-center justify-center h-full text-xs text-gray-400 mt-10">加载中...</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 弹窗容器 (Alpine 作用域外置挂载) -->
|
||||
<div id="user-modal-container" x-data="{
|
||||
showUserModal: false,
|
||||
userInfo: {},
|
||||
isMuting: false,
|
||||
muteDuration: 5,
|
||||
|
||||
// 获取用户资料
|
||||
async fetchUser(username) {
|
||||
try {
|
||||
const res = await fetch('/user/' + encodeURIComponent(username));
|
||||
this.userInfo = await res.json();
|
||||
this.showUserModal = true;
|
||||
this.isMuting = false;
|
||||
} catch (e) {
|
||||
alert('获取资料失败');
|
||||
}
|
||||
},
|
||||
|
||||
// 执行踢出
|
||||
async kickUser() {
|
||||
if (!confirm('确定要将 ' + this.userInfo.username + ' 踢出房间吗?')) return;
|
||||
try {
|
||||
const res = await fetch('/user/' + encodeURIComponent(this.userInfo.username) + '/kick', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=\"csrf-token\"]').getAttribute('content'), 'Content-Type'
|
||||
: 'application/json' , 'Accept' : 'application/json' }, body: JSON.stringify({ room_id:
|
||||
window.chatContext.roomId }) }); const data=await res.json(); if(data.status === 'success') {
|
||||
this.showUserModal=false; } else { alert('操作失败:' + data.message); } } catch (e) { alert('网络异常'); } }, // 执行禁言
|
||||
async muteUser() { try { const res=await fetch('/user/' + encodeURIComponent(this.userInfo.username) + '/mute' ,
|
||||
{ method: 'POST' , headers: { 'X-CSRF-TOKEN' :
|
||||
document.querySelector('meta[name=\"csrf-token\"]').getAttribute('content'), 'Content-Type' : 'application/json'
|
||||
, 'Accept' : 'application/json' }, body: JSON.stringify({ room_id: window.chatContext.roomId, duration:
|
||||
this.muteDuration }) }); const data=await res.json(); if(data.status === 'success') { alert(data.message);
|
||||
this.showUserModal=false; } else { alert('操作失败:' + data.message); } } catch (e) { alert('网络异常'); } } }">
|
||||
<!-- 用户名片弹窗 -->
|
||||
<div x-show="showUserModal" style="display: none;"
|
||||
class="fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div @click.away="showUserModal = false"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
|
||||
<div class="bg-gradient-to-r from-blue-500 to-indigo-600 h-24 relative">
|
||||
<button @click="showUserModal = false"
|
||||
class="absolute top-3 right-3 text-white/80 hover:text-white font-bold">×</button>
|
||||
</div>
|
||||
|
||||
<div class="px-6 pb-6 relative pt-12">
|
||||
<!-- 头像 (暂时用占位圆圈代替,后续从 /images/headface 读取) -->
|
||||
<div
|
||||
class="absolute -top-12 left-6 w-20 h-20 rounded-full border-4 border-white bg-gray-200 flex items-center justify-center text-gray-400 font-bold text-xl shadow-md">
|
||||
<img x-show="userInfo.headface" :src="'/images/headface/' + userInfo.headface"
|
||||
class="w-full h-full rounded-full object-cover"
|
||||
@error="$el.style.display='none'">
|
||||
<span x-show="!userInfo.headface">Pic</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<h3 class="text-2xl font-bold text-gray-800 flex items-center space-x-2">
|
||||
<span x-text="userInfo.username"></span>
|
||||
<span :class="userInfo.sex === '男' ? 'bg-blue-100 text-blue-700' : (userInfo.sex === '女' ? 'bg-pink-100 text-pink-700' : 'bg-gray-100 text-gray-700')" class="text-[10px] px-2 py-0.5 rounded-full" x-text="userInfo.sex"></span>
|
||||
</h3>
|
||||
<p class="text-indigo-600 text-sm font-semibold mt-1">LV.<span x-text="userInfo.user_level"></span></p>
|
||||
|
||||
<div class="mt-4 bg-gray-50 border border-gray-100 rounded-lg p-3">
|
||||
<p class="text-sm text-gray-600 italic" x-text="userInfo.sign"></p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-3 border-t pt-2">加入时间: <span x-text="userInfo.created_at"></span></p>
|
||||
</div>
|
||||
|
||||
<!-- 特权操作区(仅超管或房主显示踢人操作)-->
|
||||
@if (Auth::user()->user_level >= 15 || $room->master == Auth::user()->username)
|
||||
<div class="mt-6 pt-4 border-t border-gray-100" x-show="userInfo.username !== window.chatContext.username && userInfo.user_level < {{ Auth::user()->user_level }}">
|
||||
<p class="text-xs font-bold text-red-400 mb-2">特权操作</p>
|
||||
<div class="flex space-x-2">
|
||||
<button @click="kickUser()" class="flex-1 bg-red-100 text-red-700 hover:bg-red-200 py-1.5 rounded-md text-sm font-bold transition">踢出房间</button>
|
||||
<button @click="isMuting = !isMuting" class="flex-1 bg-amber-100 text-amber-700 hover:bg-amber-200 py-1.5 rounded-md text-sm font-bold transition">禁言拦截</button>
|
||||
</div>
|
||||
|
||||
<!-- 禁言表单 -->
|
||||
<div x-show="isMuting" class="mt-3 bg-amber-50 rounded p-2 flex items-center space-x-2 border border-amber-200" style="display: none;">
|
||||
<input type="number" x-model="muteDuration" class="w-full border-amber-300 rounded focus:ring-amber-500 text-sm px-2 py-1" placeholder="分钟" min="1">
|
||||
<span class="text-xs text-amber-800 shrink-0">分钟</span>
|
||||
<button @click="muteUser()" class="bg-amber-500 hover:bg-amber-600 text-white px-3 py-1 rounded text-sm font-bold shrink-0 shadow-sm">执行</button>
|
||||
</div>
|
||||
</div> @endif
|
||||
</div>
|
||||
|
||||
<!-- 常规操作:飞鸽传书 私信 -->
|
||||
<div class="px-6 pb-6 pt-2" x-show="userInfo.username !== window.chatContext.username">
|
||||
<a :href="'{{ route('guestbook.index', ['tab' => 'outbox']) }}&to=' + encodeURIComponent(userInfo
|
||||
.username)"
|
||||
target="_blank"
|
||||
class="w-full bg-pink-100 text-pink-700 hover:bg-pink-200 py-2.5 rounded-lg font-bold transition flex items-center justify-center shadow-sm text-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z">
|
||||
</path>
|
||||
</svg>
|
||||
飞鸽传书 (发私信)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 核心页面交互逻辑,连接 chat.js 抛出的自定义事件
|
||||
|
||||
const container = document.getElementById('chat-messages-container');
|
||||
const userList = document.getElementById('online-users-list');
|
||||
const toUserSelect = document.getElementById('to_user');
|
||||
const onlineCount = document.getElementById('online-count');
|
||||
|
||||
let onlineUsers = {}; // 用于本地维护在线名单
|
||||
|
||||
// 辅助:滚动到底部
|
||||
function scrollToBottom() {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
// 辅助:渲染在线人员列表
|
||||
function renderUserList() {
|
||||
userList.innerHTML = '';
|
||||
// 同时更新“对谁说”下拉框(保留大家选项)
|
||||
toUserSelect.innerHTML = '<option value="大家">所有人</option>';
|
||||
|
||||
let count = 0;
|
||||
for (let username in onlineUsers) {
|
||||
count++;
|
||||
let user = onlineUsers[username];
|
||||
|
||||
// 渲染右侧面板
|
||||
let li = document.createElement('li');
|
||||
li.className =
|
||||
'px-3 py-2 hover:bg-blue-50 rounded cursor-pointer transition flex items-center justify-between border-b border-gray-100 last:border-0';
|
||||
li.innerHTML = `
|
||||
<div class="flex items-center space-x-2 truncate">
|
||||
<span class="w-2 h-2 rounded-full bg-green-500 shrink-0 shadow-[0_0_5px_rgba(34,197,94,0.5)]"></span>
|
||||
<span class="text-sm font-medium text-gray-700 truncate" title="${username}">${username}</span>
|
||||
</div>
|
||||
`;
|
||||
// 单击右侧列表可以快速查看资料 / @ 人
|
||||
li.onclick = () => {
|
||||
toUserSelect.value = username;
|
||||
// 触发 Alpine 挂载的查看名片方法
|
||||
const modalScope = document.querySelector('[x-data]').__x.$data;
|
||||
if (modalScope && username !== window.chatContext.username) {
|
||||
modalScope.fetchUser(username);
|
||||
}
|
||||
};
|
||||
userList.appendChild(li);
|
||||
|
||||
// 添加到“对谁说”列表
|
||||
if (username !== window.chatContext.username) {
|
||||
let option = document.createElement('option');
|
||||
option.value = username;
|
||||
option.textContent = username;
|
||||
toUserSelect.appendChild(option);
|
||||
}
|
||||
}
|
||||
onlineCount.innerText = count;
|
||||
}
|
||||
|
||||
// 辅助:渲染单条消息气泡
|
||||
function appendMessage(msg) {
|
||||
const isMe = msg.from_user === window.chatContext.username;
|
||||
const alignClass = isMe ? 'justify-end' : 'justify-start';
|
||||
const bubbleBg = isMe ? 'bg-blue-500 text-white' : 'bg-white border border-gray-200 text-gray-800';
|
||||
const textColorAttr = msg.font_color && msg.font_color !== '#000000' && msg.font_color !== '#000' && !isMe ?
|
||||
`color: ${msg.font_color}` : '';
|
||||
|
||||
let headerText = '';
|
||||
|
||||
// 辅助:生成可点击的用户名 HTML
|
||||
const clickableUser = (uName) =>
|
||||
`<span class="cursor-pointer hover:underline hover:text-blue-600 transition" onclick="document.querySelector('[x-data]').__x.$data.fetchUser('${uName}')">${uName}</span>`;
|
||||
|
||||
if (msg.to_user !== '大家') {
|
||||
headerText = `${clickableUser(msg.from_user)} 对 ${clickableUser(msg.to_user)} ${msg.action} 说:`;
|
||||
if (msg.is_secret) headerText = `[悄悄话] ` + headerText;
|
||||
} else {
|
||||
headerText = `${clickableUser(msg.from_user)} ${msg.action} 说:`;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = `flex ${alignClass} mb-3 group`;
|
||||
|
||||
let html = `
|
||||
<div class="max-w-[75%] flex flex-col space-y-1">
|
||||
<div class="text-[11px] text-gray-400 ${isMe ? 'text-right hidden group-hover:block transition-all' : 'text-left pl-1'}">${headerText} <span class="ml-2 font-mono">${msg.sent_at}</span></div>
|
||||
<div class="px-4 py-2 rounded-2xl shadow-sm leading-relaxed whitespace-pre-wrap word-break ${bubbleBg}" style="${textColorAttr}">${msg.content}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
div.innerHTML = html;
|
||||
container.appendChild(div);
|
||||
scrollToBottom();
|
||||
}
|
||||
|
||||
// 🚀 初始化 WebSocket 监听器
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (typeof window.initChat === 'function') {
|
||||
window.initChat(window.chatContext.roomId);
|
||||
}
|
||||
});
|
||||
|
||||
// 🔌 监听 WebSocket 事件
|
||||
window.addEventListener('chat:here', (e) => {
|
||||
const users = e.detail;
|
||||
onlineUsers = {};
|
||||
users.forEach(u => {
|
||||
onlineUsers[u.username] = u;
|
||||
});
|
||||
renderUserList();
|
||||
});
|
||||
|
||||
window.addEventListener('chat:joining', (e) => {
|
||||
const user = e.detail;
|
||||
onlineUsers[user.username] = user;
|
||||
renderUserList();
|
||||
|
||||
// 可选:渲染一条系统提示“某某加入了房间”
|
||||
});
|
||||
|
||||
window.addEventListener('chat:leaving', (e) => {
|
||||
const user = e.detail;
|
||||
delete onlineUsers[user.username];
|
||||
renderUserList();
|
||||
});
|
||||
|
||||
window.addEventListener('chat:message', (e) => {
|
||||
const msg = e.detail;
|
||||
// 过滤私聊:如果是别人对别人的悄悄话,自己不应该显示
|
||||
if (msg.is_secret && msg.from_user !== window.chatContext.username && msg.to_user !== window.chatContext
|
||||
.username) {
|
||||
return;
|
||||
}
|
||||
appendMessage(msg);
|
||||
});
|
||||
|
||||
window.addEventListener('chat:kicked', (e) => {
|
||||
if (e.detail.username === window.chatContext.username) {
|
||||
alert("您已被管理员踢出房间!");
|
||||
window.location.href = "{{ route('home') }}";
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('chat:title-updated', (e) => {
|
||||
document.getElementById('room-title-display').innerText = e.detail.title;
|
||||
});
|
||||
|
||||
|
||||
// 📤 发送消息逻辑
|
||||
document.getElementById('content').addEventListener('keydown', function(e) {
|
||||
if (e.ctrlKey && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
sendMessage(e);
|
||||
}
|
||||
});
|
||||
|
||||
async function sendMessage(e) {
|
||||
if (e) e.preventDefault();
|
||||
|
||||
const form = document.getElementById('chat-form');
|
||||
const formData = new FormData(form);
|
||||
const contentInput = document.getElementById('content');
|
||||
const submitBtn = document.getElementById('send-btn');
|
||||
|
||||
const content = formData.get('content').trim();
|
||||
if (!content) {
|
||||
contentInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// 锁定按钮防连点
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.classList.add('opacity-50', 'cursor-not-allowed');
|
||||
|
||||
try {
|
||||
const response = await fetch(window.chatContext.sendUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||
'content'),
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok && data.status === 'success') {
|
||||
// 发送成功,清空刚才的输入并获取焦点
|
||||
contentInput.value = '';
|
||||
contentInput.focus();
|
||||
} else {
|
||||
alert('发送失败: ' + (data.message || JSON.stringify(data.errors)));
|
||||
}
|
||||
} catch (error) {
|
||||
alert('网络连接错误,消息发送失败!');
|
||||
console.error(error);
|
||||
} finally {
|
||||
// 解锁按钮
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.classList.remove('opacity-50', 'cursor-not-allowed');
|
||||
}
|
||||
}
|
||||
|
||||
// 🚪 退出房间逻辑
|
||||
async function leaveRoom() {
|
||||
if (!confirm('确定要离开聊天室吗?')) return;
|
||||
|
||||
try {
|
||||
await fetch(window.chatContext.leaveUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||
'content'),
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
window.location.href = "{{ route('home') }}";
|
||||
}
|
||||
|
||||
// ⏳ 自动挂机心跳 (每 3 分钟执行一次)
|
||||
const HEARTBEAT_INTERVAL = 180 * 1000;
|
||||
setInterval(async () => {
|
||||
if (!window.chatContext || !window.chatContext.heartbeatUrl) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(window.chatContext.heartbeatUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute(
|
||||
'content'),
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok && data.status === 'success') {
|
||||
// 可选:在这里如果需要更新自己的名片经验条,可触发 Alpine 等级更新(如果实现了前台独立显示自己经验的功能的话)
|
||||
console.log('心跳存点成功,当前经验值:' + data.data.exp_num + ', 等级:' + data.data.user_level);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('挂机心跳断开', e);
|
||||
}
|
||||
}, HEARTBEAT_INTERVAL);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
244
resources/views/guestbook/index.blade.php
Normal file
244
resources/views/guestbook/index.blade.php
Normal file
@@ -0,0 +1,244 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '星光留言板')
|
||||
|
||||
@section('content')
|
||||
<div class="h-screen w-full flex flex-col bg-gray-50 overflow-hidden font-sans" x-data="{ showWriteForm: false, towho: '{{ $defaultTo }}' }">
|
||||
|
||||
<!-- 顶部导航条 -->
|
||||
<header class="bg-indigo-900 border-b border-indigo-800 text-white shadow-md relative z-20 shrink-0">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- 左侧:标题与返回大厅 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="{{ route('rooms.index') }}"
|
||||
class="text-indigo-200 hover:text-white transition flex items-center group">
|
||||
<svg class="w-5 h-5 mr-1 transform group-hover:-translate-x-1 transition" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
返回大厅
|
||||
</a>
|
||||
<div class="h-6 w-px bg-indigo-700"></div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xl">✉️</span>
|
||||
<h1 class="text-xl font-extrabold tracking-wider">星光留言板</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:写留言按钮 -->
|
||||
<button
|
||||
@click="showWriteForm = !showWriteForm; if(showWriteForm) setTimeout(() => $refs.textBody.focus(), 100)"
|
||||
class="bg-indigo-500 hover:bg-indigo-400 text-white px-4 py-2 rounded-lg font-bold shadow-sm transition flex items-center">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z">
|
||||
</path>
|
||||
</svg>
|
||||
<span x-text="showWriteForm ? '取消留言' : '发布留言 / 写信'"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (session('success'))
|
||||
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-4 mx-4 mt-4 shadow-sm"
|
||||
role="alert">
|
||||
<p>{{ session('success') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4 mx-4 mt-4 shadow-sm" role="alert">
|
||||
<p>{{ session('error') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-4 mx-4 mt-4 shadow-sm">
|
||||
<ul class="list-disc pl-5">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- 写信/留言表单区 (Alpine 控制显隐) -->
|
||||
<div x-show="showWriteForm" x-collapse class="bg-white border-b border-gray-200 shadow-inner">
|
||||
<div class="max-w-4xl mx-auto p-6">
|
||||
<form action="{{ route('guestbook.store') }}" method="POST" class="space-y-4">
|
||||
@csrf
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-1">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">接收人 (留空或填“大家”表示公共留言)</label>
|
||||
<input type="text" name="towho" x-model="towho" placeholder="系统自动处理"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
</div>
|
||||
<div class="flex items-center h-full pt-6">
|
||||
<label
|
||||
class="flex items-center space-x-2 text-sm text-gray-700 cursor-pointer bg-pink-50 px-3 py-2 rounded-md hover:bg-pink-100 transition border border-pink-100">
|
||||
<input type="checkbox" name="secret" value="1"
|
||||
class="rounded text-pink-500 focus:ring-pink-500 w-4 h-4">
|
||||
<span class="font-bold text-pink-700">🔒 悄悄话 (仅双方可见)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">正文内容 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<textarea name="text_body" x-ref="textBody" rows="3" required
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
placeholder="相逢何必曾相识,留下您的足迹吧..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-6 rounded-md shadow flex items-center font-bold">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"></path>
|
||||
</svg>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容区 -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
|
||||
<!-- 左侧:分类导航 -->
|
||||
<div class="w-64 bg-white border-r border-gray-200 shrink-0 hidden md:block">
|
||||
<div class="p-6">
|
||||
<nav class="space-y-2">
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'public']) }}"
|
||||
class="flex items-center px-4 py-3 rounded-lg font-medium transition-colors {{ $tab === 'public' ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50' }}">
|
||||
<span class="mr-3 text-lg">🌍</span> 公共留言墙
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'inbox']) }}"
|
||||
class="flex items-center px-4 py-3 rounded-lg font-medium transition-colors {{ $tab === 'inbox' ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50' }}">
|
||||
<span class="mr-3 text-lg">📥</span> 我收件的
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'outbox']) }}"
|
||||
class="flex items-center px-4 py-3 rounded-lg font-medium transition-colors {{ $tab === 'outbox' ? 'bg-indigo-50 text-indigo-700' : 'text-gray-600 hover:bg-gray-50' }}">
|
||||
<span class="mr-3 text-lg">📤</span> 我发出的
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:留言流列表 -->
|
||||
<main class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8 bg-gray-50">
|
||||
<div class="max-w-4xl mx-auto space-y-4">
|
||||
|
||||
@forelse($messages as $msg)
|
||||
@php
|
||||
// 判断是否属于自己发或收的悄悄话,用于高亮
|
||||
$isSecret = $msg->secret == 1;
|
||||
$isToMe = $msg->towho === Auth::user()->username;
|
||||
$isFromMe = $msg->who === Auth::user()->username;
|
||||
@endphp
|
||||
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm border {{ $isSecret ? 'border-pink-200' : 'border-gray-200' }} p-5 relative group overflow-hidden">
|
||||
|
||||
<!-- 悄悄话角标 -->
|
||||
@if ($isSecret)
|
||||
<div
|
||||
class="absolute top-0 right-0 bg-pink-100 text-pink-700 text-[10px] font-bold px-2 py-1 rounded-bl-lg">
|
||||
🔒 私密信件
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div class="flex items-center text-sm">
|
||||
<span class="font-bold text-indigo-700">{{ $msg->who }}</span>
|
||||
<span class="text-gray-400 mx-2">给</span>
|
||||
<span
|
||||
class="font-bold {{ $msg->towho ? 'text-indigo-700' : 'text-gray-500' }}">{{ $msg->towho ?: '大家' }}</span>
|
||||
<span class="text-gray-400 mx-2">留言:</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 flex items-center space-x-3">
|
||||
<span>{{ \Carbon\Carbon::parse($msg->post_time)->diffForHumans() }}</span>
|
||||
|
||||
<!-- 删除按钮 (只有发件人、收件人、超管可见) -->
|
||||
@if ($isFromMe || $isToMe || Auth::user()->user_level >= 15)
|
||||
<form action="{{ route('guestbook.destroy', $msg->id) }}" method="POST"
|
||||
onsubmit="return confirm('确定要抹除这条留言吗?');" class="inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit"
|
||||
class="text-red-400 hover:text-red-600 transition opacity-0 group-hover:opacity-100 bg-red-50 px-2 py-1 rounded">
|
||||
删除
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-gray-800 leading-relaxed text-sm whitespace-pre-wrap {{ $isSecret ? 'bg-pink-50 p-3 rounded-lg border border-pink-100' : 'bg-gray-50 p-3 rounded-lg border border-gray-100' }}">
|
||||
{!! nl2br(e($msg->text_body)) !!}
|
||||
</div>
|
||||
|
||||
<!-- 快捷回复按钮 -->
|
||||
@if ($msg->who !== Auth::user()->username)
|
||||
<div class="mt-3 flex justify-end">
|
||||
<button
|
||||
@click="showWriteForm = true; towho = '{{ $msg->who }}'; setTimeout(() => $refs.textBody.focus(), 100); window.scrollTo({top:0, behavior:'smooth'})"
|
||||
class="text-xs text-indigo-500 hover:text-indigo-700 font-medium flex items-center transition">
|
||||
<svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"></path>
|
||||
</svg>
|
||||
回复TA
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
||||
<span class="text-4xl">📭</span>
|
||||
<h3 class="mt-4 text-lg font-medium text-gray-900">暂无信件</h3>
|
||||
<p class="mt-2 text-sm text-gray-500">这里是空空如也的荒原。</p>
|
||||
<button
|
||||
@click="showWriteForm = true; towho = ''; setTimeout(() => $refs.textBody.focus(), 100)"
|
||||
class="mt-4 text-indigo-600 font-bold hover:underline">
|
||||
来抢沙发留言吧!
|
||||
</button>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="mt-6">
|
||||
{{ $messages->links() }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端底部分类栏 -->
|
||||
<div class="md:hidden bg-white border-t border-gray-200 flex justify-around p-2 shrink-0">
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'public']) }}"
|
||||
class="flex flex-col items-center {{ $tab === 'public' ? 'text-indigo-600' : 'text-gray-500' }}">
|
||||
<span class="text-xl">🌍</span>
|
||||
<span class="text-xs mt-1">公共墙</span>
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'inbox']) }}"
|
||||
class="flex flex-col items-center {{ $tab === 'inbox' ? 'text-indigo-600' : 'text-gray-500' }}">
|
||||
<span class="text-xl">📥</span>
|
||||
<span class="text-xs mt-1">收件箱</span>
|
||||
</a>
|
||||
<a href="{{ route('guestbook.index', ['tab' => 'outbox']) }}"
|
||||
class="flex flex-col items-center {{ $tab === 'outbox' ? 'text-indigo-600' : 'text-gray-500' }}">
|
||||
<span class="text-xl">📤</span>
|
||||
<span class="text-xs mt-1">发件箱</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
130
resources/views/index.blade.php
Normal file
130
resources/views/index.blade.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ \App\Models\SysParam::where('alias', 'sys_name')->value('body') ?? '飘落的流星在线聊天' }} - 登录</title>
|
||||
<!-- 使用现代 CDN 引入 Tailwind CSS (快速构建 UI) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
</head>
|
||||
<body class="bg-gray-100 h-screen flex items-center justify-center">
|
||||
|
||||
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
|
||||
<h1 class="text-2xl font-bold text-center mb-6 text-gray-800">
|
||||
{{ \App\Models\SysParam::where('alias', 'sys_name')->value('body') ?? '在线聊天室' }}
|
||||
</h1>
|
||||
|
||||
<p class="text-sm text-gray-500 text-center mb-6">
|
||||
{{ \App\Models\SysParam::where('alias', 'sys_notice')->value('body') ?? '欢迎您的加入' }}
|
||||
</p>
|
||||
|
||||
<!-- 登录提示区 -->
|
||||
<div id="alert-box" class="hidden mb-4 p-3 rounded text-sm text-center"></div>
|
||||
|
||||
<form id="login-form" class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700">昵称 (第一次登录即注册)</label>
|
||||
<input type="text" id="username" name="username" required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="允许中英文、数字、下划线">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">密码</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="请输入密码">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="captcha" class="block text-sm font-medium text-gray-700">验证码</label>
|
||||
<div class="mt-1 flex space-x-2">
|
||||
<input type="text" id="captcha" name="captcha" required
|
||||
class="block w-2/3 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="输入右侧字符">
|
||||
<div class="w-1/3 cursor-pointer" onclick="refreshCaptcha()">
|
||||
<!-- 验证码图片,点击刷新 -->
|
||||
<img src="{{ captcha_src() }}" alt="验证码" id="captcha-img" class="h-full w-full rounded-md border border-gray-300 object-cover">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submit-btn"
|
||||
class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50">
|
||||
进入聊天室
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 刷新验证码
|
||||
function refreshCaptcha() {
|
||||
document.getElementById('captcha-img').src = '{{ captcha_src() }}' + Math.random();
|
||||
}
|
||||
|
||||
// 提交登录表单
|
||||
document.getElementById('login-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('submit-btn');
|
||||
const alertBox = document.getElementById('alert-box');
|
||||
|
||||
btn.disabled = true;
|
||||
btn.innerText = '正在进入...';
|
||||
alertBox.classList.add('hidden');
|
||||
|
||||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
fetch('{{ route("login.post") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
'Accept': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json().then(data => ({ status: response.status, body: data })))
|
||||
.then(result => {
|
||||
if (result.status === 200 && result.body.status === 'success') {
|
||||
// 登录成功,显示成功并跳转
|
||||
showAlert(result.body.message, 'success');
|
||||
setTimeout(() => {
|
||||
// TODO: 之后重定向到真实的聊天室页面 /chat
|
||||
window.location.href = '/';
|
||||
}, 1000);
|
||||
} else {
|
||||
// 验证失败或密码错误
|
||||
const errorMsg = result.body.message || (result.body.errors ? Object.values(result.body.errors)[0][0] : '登录失败');
|
||||
showAlert(errorMsg, 'error');
|
||||
refreshCaptcha();
|
||||
document.getElementById('captcha').value = '';
|
||||
btn.disabled = false;
|
||||
btn.innerText = '进入聊天室';
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showAlert('网络或服务器错误,请稍后再试。', 'error');
|
||||
refreshCaptcha();
|
||||
btn.disabled = false;
|
||||
btn.innerText = '进入聊天室';
|
||||
});
|
||||
});
|
||||
|
||||
function showAlert(message, type) {
|
||||
const box = document.getElementById('alert-box');
|
||||
box.innerText = message;
|
||||
box.classList.remove('hidden', 'bg-red-100', 'text-red-700', 'bg-green-100', 'text-green-700');
|
||||
|
||||
if (type === 'error') {
|
||||
box.classList.add('bg-red-100', 'text-red-700');
|
||||
} else {
|
||||
box.classList.add('bg-green-100', 'text-green-700');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
131
resources/views/leaderboard/index.blade.php
Normal file
131
resources/views/leaderboard/index.blade.php
Normal file
@@ -0,0 +1,131 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '风云排行榜')
|
||||
|
||||
@section('content')
|
||||
<div class="h-screen w-full flex flex-col bg-gray-100 overflow-hidden font-sans">
|
||||
|
||||
<!-- 顶部导航条 -->
|
||||
<header class="bg-indigo-900 border-b border-indigo-800 text-white shadow-md relative z-20 shrink-0">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center justify-between h-16">
|
||||
<!-- 左侧:标题与返回大厅 -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="{{ route('rooms.index') }}"
|
||||
class="text-indigo-200 hover:text-white transition flex items-center group">
|
||||
<svg class="w-5 h-5 mr-1 transform group-hover:-translate-x-1 transition" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
返回大厅
|
||||
</a>
|
||||
<div class="h-6 w-px bg-indigo-700"></div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-xl">🏆</span>
|
||||
<h1 class="text-xl font-extrabold tracking-wider">风云排行榜</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:当前用户状态 -->
|
||||
<div class="flex items-center space-x-3 text-sm">
|
||||
<img src="/images/headface/{{ Auth::user()->headface ?? '01.gif' }}"
|
||||
class="w-8 h-8 rounded border border-indigo-500 object-cover bg-white">
|
||||
<div class="hidden sm:block">
|
||||
<span class="font-bold">{{ Auth::user()->username }}</span>
|
||||
<span class="text-indigo-300 ml-2">LV.{{ Auth::user()->user_level }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 说明条 -->
|
||||
<div class="bg-indigo-50 border-b border-indigo-100 py-3 shrink-0">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center text-sm">
|
||||
<p class="text-indigo-800 font-medium">✨ 数据每 <span class="font-bold text-red-500">15分钟</span>
|
||||
自动刷新一次。努力提升自己,让全服铭记你的名字!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 滚动内容区 -->
|
||||
<main class="flex-1 overflow-y-auto p-4 sm:p-6 lg:p-8">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
|
||||
<!-- 1. 境界榜 (user_level) -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-red-600 to-red-500 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center"><span class="mr-2 text-xl">👑</span> 无上境界榜</h2>
|
||||
<span class="text-xs bg-red-800/40 px-2 py-0.5 rounded">Level</span>
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topLevels,
|
||||
'valueField' => 'user_level',
|
||||
'unit' => '级',
|
||||
'color' => 'text-red-600',
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 修为榜 (exp_num) -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-amber-600 to-amber-500 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center"><span class="mr-2 text-xl">🔥</span> 苦修经验榜</h2>
|
||||
<span class="text-xs bg-amber-800/40 px-2 py-0.5 rounded">Exp</span>
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topExp,
|
||||
'valueField' => 'exp_num',
|
||||
'unit' => '点',
|
||||
'color' => 'text-amber-600',
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. 财富榜 (jjb) -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-yellow-500 to-yellow-400 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center text-yellow-900"><span
|
||||
class="mr-2 text-xl">💰</span> 盖世神豪榜</h2>
|
||||
<span class="text-xs bg-yellow-800/20 text-yellow-900 px-2 py-0.5 rounded">Coin</span>
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topWealth,
|
||||
'valueField' => 'jjb',
|
||||
'unit' => '万',
|
||||
'color' => 'text-yellow-600',
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 魅力榜 (meili) -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden flex flex-col">
|
||||
<div
|
||||
class="bg-gradient-to-r from-pink-600 to-pink-500 px-4 py-3 flex justify-between items-center text-white">
|
||||
<h2 class="font-bold text-lg flex items-center"><span class="mr-2 text-xl">🌸</span> 绝世名伶榜</h2>
|
||||
<span class="text-xs bg-pink-800/40 px-2 py-0.5 rounded">Charm</span>
|
||||
</div>
|
||||
<div class="p-0 overflow-y-auto max-h-[600px] flex-1">
|
||||
@include('leaderboard.partials.list', [
|
||||
'users' => $topCharm,
|
||||
'valueField' => 'meili',
|
||||
'unit' => '点',
|
||||
'color' => 'text-pink-600',
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@endsection
|
||||
57
resources/views/leaderboard/partials/list.blade.php
Normal file
57
resources/views/leaderboard/partials/list.blade.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<ul class="divide-y divide-gray-100">
|
||||
@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
|
||||
<li class="p-3 flex items-center justify-between transition-colors duration-150 {{ $rowBg }}">
|
||||
<!-- 左侧:名次与头像/名字 -->
|
||||
<div class="flex items-center space-x-3 overflow-hidden">
|
||||
<div
|
||||
class="w-6 h-6 shrink-0 {{ $rankBg }} rounded-full flex items-center justify-center font-bold text-xs">
|
||||
{{ $index + 1 }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 truncate">
|
||||
<img class="w-8 h-8 rounded border object-cover shrink-0"
|
||||
src="/images/headface/{{ $user->headface ?? '01.gif' }}" alt="">
|
||||
<div class="flex flex-col truncate">
|
||||
<span class="text-sm font-bold text-gray-800 truncate" title="{{ $user->username }}">
|
||||
{{ $user->username }}
|
||||
@if ($user->sex == '女')
|
||||
<span class="text-pink-500 text-xs ml-0.5">♀</span>
|
||||
@elseif($user->sex == '男')
|
||||
<span class="text-blue-500 text-xs ml-0.5">♂</span>
|
||||
@endif
|
||||
</span>
|
||||
<span class="text-[10px] text-gray-500 truncate"
|
||||
title="{{ $user->sign }}">{{ $user->sign ?: '这家伙很懒,什么也没留下' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:数值 -->
|
||||
<div class="flex flex-col items-end shrink-0 ml-2">
|
||||
<span class="text-sm font-black {{ $index < 3 ? $color : 'text-gray-600' }}">
|
||||
{{ number_format($user->$valueField) }}
|
||||
<span
|
||||
class="text-[10px] font-normal {{ $index < 3 ? $color : 'text-gray-400' }} ml-0.5">{{ $unit }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
@empty
|
||||
<li class="p-8 text-center text-sm text-gray-400 font-bold">
|
||||
暂无数据登榜
|
||||
</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
434
resources/views/rooms/index.blade.php
Normal file
434
resources/views/rooms/index.blade.php
Normal file
@@ -0,0 +1,434 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>聊天大厅 - 飘落流星</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- 引入 Alpine.js 用于简单的无代码弹窗切换 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<style>
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-color: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 min-h-screen text-gray-800" x-data="{
|
||||
showCreateModal: false,
|
||||
showEditModal: false,
|
||||
showTransferModal: false,
|
||||
showProfileModal: false,
|
||||
showPasswordModal: false,
|
||||
currentRoom: null
|
||||
}">
|
||||
|
||||
<!-- 顶部导航条 -->
|
||||
<nav class="bg-indigo-600 px-6 py-4 shadow-md sticky top-0 z-50">
|
||||
<div class="max-w-7xl mx-auto flex justify-between items-center text-white">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-xl">🌟</span>
|
||||
<h1 class="text-xl font-extrabold tracking-wider">星光大厅</h1>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
<div class="flex items-center space-x-3 text-sm">
|
||||
|
||||
<!-- 留言板入口 -->
|
||||
<a href="{{ route('guestbook.index') }}"
|
||||
class="text-indigo-100 hover:text-white font-bold flex items-center bg-indigo-800/40 px-3 py-1.5 rounded-full transition shadow-inner">
|
||||
<span class="mr-1">✉️</span> 留言板
|
||||
</a>
|
||||
|
||||
<!-- 风云排行榜入口 -->
|
||||
<a href="{{ route('leaderboard.index') }}"
|
||||
class="mr-4 text-yellow-400 hover:text-yellow-300 font-bold flex items-center bg-indigo-800/50 px-3 py-1.5 rounded-full transition shadow-inner">
|
||||
<span class="mr-1">🏆</span> 风云榜
|
||||
</a>
|
||||
|
||||
<!-- admin 后台直达 -->
|
||||
@if (Auth::user()->user_level >= 15)
|
||||
<a href="{{ route('admin.dashboard') }}"
|
||||
class="mr-4 text-indigo-200 hover:text-white font-bold hidden sm:block">⚙️ 后台管理</a>
|
||||
@endif
|
||||
<!-- 点击直接在本页弹出资料卡修改 -->
|
||||
<button @click="showProfileModal = true"
|
||||
class="font-medium text-sm hover:text-indigo-200 transition flex items-center">
|
||||
欢迎您,{{ Auth::user()->username }}
|
||||
<span
|
||||
class="bg-white/20 px-2 py-0.5 rounded-full text-xs ml-2 border border-white/10 shadow-sm">LV.{{ Auth::user()->user_level }}</span>
|
||||
</button>
|
||||
|
||||
{{-- 权限按钮区 --}}
|
||||
@if (Auth::user()->user_level >= 10)
|
||||
<button @click="showCreateModal = true"
|
||||
class="bg-emerald-500 hover:bg-emerald-400 px-4 py-2 rounded-md font-bold text-sm transition shadow-sm">
|
||||
+ 新建房间
|
||||
</button>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('logout') }}" method="POST" class="inline">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="text-sm border border-white/30 hover:bg-white/10 px-4 py-2 rounded-md transition font-medium">退出登录</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 主展示区 -->
|
||||
<main class="max-w-7xl mx-auto py-10 px-6">
|
||||
|
||||
<!-- 全局提示消息 -->
|
||||
@if (session('success'))
|
||||
<div class="bg-green-100 border-l-4 border-green-500 text-green-700 p-4 mb-8 rounded shadow-sm">
|
||||
<p class="font-bold">操作成功</p>
|
||||
<p>{{ session('success') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if (session('error'))
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-8 rounded shadow-sm">
|
||||
<p class="font-bold">发生错误</p>
|
||||
<p>{{ session('error') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 mb-8 rounded shadow-sm">
|
||||
<ul class="list-disc list-inside text-sm">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mb-6 flex justify-between items-end border-b pb-4">
|
||||
<h2 class="text-xl font-bold text-gray-700">公开频段 (<span
|
||||
class="text-indigo-600">{{ $rooms->count() }}</span>)</h2>
|
||||
</div>
|
||||
|
||||
<!-- 房间瀑布流网格 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
@forelse($rooms as $room)
|
||||
<div
|
||||
class="bg-white rounded-xl shadow-sm hover:shadow-lg transition-shadow border border-gray-100 overflow-hidden flex flex-col group {{ $room->is_system ? 'ring-2 ring-indigo-500/20' : '' }}">
|
||||
|
||||
{{-- 卡片头部 --}}
|
||||
<div class="p-5 flex-1 relative">
|
||||
@if ($room->is_system)
|
||||
<span
|
||||
class="absolute top-4 right-4 bg-indigo-100 text-indigo-700 text-xs font-bold px-2 py-1 rounded-sm">官方驻地</span>
|
||||
@endif
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-2 truncate" title="{{ $room->name }}">
|
||||
{{ $room->name }}</h3>
|
||||
<p class="text-sm text-gray-500 line-clamp-2 h-10 mb-4">
|
||||
{{ $room->description ?: '房主很懒,什么都没写...' }}</p>
|
||||
<div class="flex items-center text-xs text-gray-400 font-medium">
|
||||
<span>房主:<span
|
||||
class="{{ $room->master == Auth::user()->username ? 'text-indigo-600 font-bold' : 'text-gray-600' }}">{{ $room->master }}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 底部操作区 --}}
|
||||
<div class="bg-gray-50 p-4 border-t border-gray-100 flex items-center justify-between">
|
||||
|
||||
{{-- 管理按钮组(仅房主或超管可见) --}}
|
||||
<div class="flex space-x-2">
|
||||
@if ($room->master == Auth::user()->username || Auth::user()->user_level >= 15)
|
||||
<!-- 修改 -->
|
||||
<button
|
||||
@click="currentRoom = {id: {{ $room->id }}, name: '{{ addslashes($room->name) }}', description: '{{ addslashes($room->description) }}'}; showEditModal = true"
|
||||
class="text-xs text-blue-600 hover:text-blue-800 font-semibold px-2 py-1 rounded hover:bg-blue-50 transition">
|
||||
管理
|
||||
</button>
|
||||
|
||||
@if (!$room->is_system)
|
||||
<!-- 转让 -->
|
||||
<button
|
||||
@click="currentRoom = {id: {{ $room->id }}, name: '{{ addslashes($room->name) }}'}; showTransferModal = true"
|
||||
class="text-xs text-amber-600 hover:text-amber-800 font-semibold px-2 py-1 rounded hover:bg-amber-50 transition">
|
||||
转让
|
||||
</button>
|
||||
<!-- 删除 -->
|
||||
<form action="{{ route('rooms.destroy', $room->id) }}" method="POST"
|
||||
class="inline"
|
||||
onsubmit="return confirm('警告:确实要彻底解散「{{ $room->name }}」吗?此操作不可逆!');">
|
||||
@csrf @method('delete')
|
||||
<button type="submit"
|
||||
class="text-xs text-red-600 hover:text-red-800 font-semibold px-2 py-1 rounded hover:bg-red-50 transition">解散</button>
|
||||
</form>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 进入按钮 --}}
|
||||
<a href="{{ route('chat.room', $room->id) }}"
|
||||
class="bg-indigo-600 text-white hover:bg-indigo-700 px-4 py-2 rounded-t-xl rounded-br-xl text-sm font-bold shadow-md hover:shadow-lg transition-all transform group-hover:-translate-y-0.5">
|
||||
立刻进入 →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="col-span-full py-20 text-center text-gray-400">
|
||||
<p class="text-xl font-bold mb-2">大厅空空如也</p>
|
||||
<p>目前还没有人创建任何房间。</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 新建房间 Modal (通过 Alpine.js 开关) -->
|
||||
<div x-show="showCreateModal" style="display: none;"
|
||||
class="fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div @click.away="showCreateModal = false"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
<div class="bg-gray-50 border-b px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="font-bold text-lg text-gray-800">新建私人聊天频道</h3>
|
||||
<button @click="showCreateModal = false"
|
||||
class="text-gray-400 hover:text-gray-600 font-bold text-xl">×</button>
|
||||
</div>
|
||||
<form action="{{ route('rooms.store') }}" method="POST" class="p-6">
|
||||
@csrf
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">频道名称 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<input type="text" name="name" required maxlength="50" placeholder="例如:技术交流水群"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">频道主题描述</label>
|
||||
<textarea name="description" rows="3" maxlength="255" placeholder="一句话介绍这里的规矩..."
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border resize-none"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="showCreateModal = false"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium">取消</button>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-emerald-600 text-white rounded-md font-bold hover:bg-emerald-700 shadow-sm">立即创建</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改管理 Modal -->
|
||||
<div x-show="showEditModal" style="display: none;"
|
||||
class="fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div @click.away="showEditModal = false"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
<div class="bg-blue-50 border-b border-blue-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="font-bold text-lg text-blue-900" x-text="'管理频道: ' + currentRoom?.name"></h3>
|
||||
<button @click="showEditModal = false"
|
||||
class="text-blue-400 hover:text-blue-600 font-bold text-xl">×</button>
|
||||
</div>
|
||||
<!-- 注意这里通过 Alpine 动态拼接 action 路径 -->
|
||||
<form :action="'{{ url('rooms') }}/' + currentRoom?.id" method="POST" class="p-6">
|
||||
@csrf @method('PUT')
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">频道新名称 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<input type="text" name="name" :value="currentRoom?.name" required maxlength="50"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">更新主题描述</label>
|
||||
<textarea name="description" x-text="currentRoom?.description" rows="3" maxlength="255"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500 p-2.5 bg-gray-50 border resize-none"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="showEditModal = false"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium">关闭</button>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-blue-600 text-white rounded-md font-bold hover:bg-blue-700 shadow-sm">保存并广播</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 转让房主 Modal -->
|
||||
<div x-show="showTransferModal" style="display: none;"
|
||||
class="fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div @click.away="showTransferModal = false"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
<div class="bg-amber-50 border-b border-amber-100 px-6 py-4 flex justify-between items-center">
|
||||
<h3 class="font-bold text-lg text-amber-900" x-text="'转让授权: ' + currentRoom?.name"></h3>
|
||||
<button @click="showTransferModal = false"
|
||||
class="text-amber-400 hover:text-amber-600 font-bold text-xl">×</button>
|
||||
</div>
|
||||
<form :action="'{{ url('rooms') }}/' + currentRoom?.id + '/transfer'" method="POST" class="p-6">
|
||||
@csrf
|
||||
<div class="bg-amber-100 text-amber-800 text-xs p-3 rounded mb-4">
|
||||
转让后您将立即失去对该房间的管理权限,请谨慎操作。
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">接收人昵称 <span
|
||||
class="text-red-500">*</span></label>
|
||||
<input type="text" name="target_username" required placeholder="请输入对方准确用户名"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-amber-500 focus:ring-amber-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" @click="showTransferModal = false"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium">取消</button>
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-amber-600 text-white rounded-md font-bold hover:bg-amber-700 shadow-sm">确认转让</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 个人资料设置 Modal -->
|
||||
<div x-show="showProfileModal" style="display: none;"
|
||||
class="fixed inset-0 z-[100] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div @click.away="showProfileModal = false"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
<div
|
||||
class="bg-gradient-to-r from-indigo-500 to-purple-600 px-6 py-4 flex justify-between items-center text-white">
|
||||
<h3 class="font-bold text-lg">个人资料设置</h3>
|
||||
<button @click="showProfileModal = false"
|
||||
class="text-white/80 hover:text-white font-bold text-xl">×</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6" x-data="{
|
||||
profileData: {
|
||||
sex: '{{ Auth::user()->sex }}',
|
||||
sign: '{{ Auth::user()->sign }}',
|
||||
headface: '{{ Auth::user()->headface ?? '01.gif' }}'
|
||||
},
|
||||
isSaving: false,
|
||||
|
||||
async saveProfile() {
|
||||
this.isSaving = true;
|
||||
try {
|
||||
const res = await fetch('{{ route('user.update_profile') }}', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=\"csrf-token\"]').getAttribute('content'), 'Content-Type'
|
||||
: 'application/json' , 'Accept' : 'application/json' }, body: JSON.stringify(this.profileData) }); const
|
||||
data=await res.json(); if(res.ok && data.status === 'success') { alert(data.message);
|
||||
window.location.reload(); } else { alert('保存失败: ' + (data.message || ' 输入有误')); } } catch (e) {
|
||||
alert('网络异常'); } finally { this.isSaving=false; } } }">
|
||||
<form @submit.prevent="saveProfile">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">性别</label>
|
||||
<select x-model="profileData.sex"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border">
|
||||
<option value="男">男生</option>
|
||||
<option value="女">女生</option>
|
||||
<option value="保密">保密</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- 头像选择 (暂时写死输入框,后续可优化为网格选择) -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">头像选择 (01.gif - 50.gif)</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-12 h-12 rounded bg-gray-200 shrink-0 overflow-hidden border">
|
||||
<img :src="'/images/headface/' + profileData.headface"
|
||||
@error="$el.style.display='none'" class="w-full h-full object-cover">
|
||||
</div>
|
||||
<input type="text" x-model="profileData.headface" required
|
||||
class="flex-1 border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">个性签名</label>
|
||||
<textarea x-model="profileData.sign" rows="2" maxlength="255" placeholder="写点什么吧..."
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2.5 bg-gray-50 border resize-none"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-between items-center border-t pt-4">
|
||||
<button type="button" @click="showProfileModal = false; showPasswordModal = true"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800 font-bold hover:underline">修改安全密码</button>
|
||||
<div class="space-x-2">
|
||||
<button type="button" @click="showProfileModal = false"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium cursor-pointer">取消</button>
|
||||
<button type="submit" :disabled="isSaving"
|
||||
class="px-6 py-2 bg-indigo-600 text-white rounded-md font-bold hover:bg-indigo-700 shadow-sm disabled:opacity-50 cursor-pointer">保存资料</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码 Modal -->
|
||||
<div x-show="showPasswordModal" style="display: none;"
|
||||
class="fixed inset-0 z-[110] bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<div @click.away="showPasswordModal = false"
|
||||
class="bg-white rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden transform transition-all"
|
||||
x-transition.scale.95>
|
||||
<div class="bg-gray-800 px-6 py-4 flex justify-between items-center text-white">
|
||||
<h3 class="font-bold text-lg">修改安全密码</h3>
|
||||
<button @click="showPasswordModal = false"
|
||||
class="text-gray-400 hover:text-white font-bold text-xl">×</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6" x-data="{
|
||||
pwdData: {
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
new_password_confirmation: ''
|
||||
},
|
||||
isSaving: false,
|
||||
|
||||
async savePassword() {
|
||||
if (this.pwdData.new_password !== this.pwdData.new_password_confirmation) {
|
||||
alert('两次输入的新密码不一致!');
|
||||
return;
|
||||
}
|
||||
if (this.pwdData.new_password.length < 6) {
|
||||
alert('新密码最少 6 位!');
|
||||
return;
|
||||
}
|
||||
this.isSaving = true;
|
||||
try {
|
||||
const res = await fetch('{{ route('user.update_password') }}', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name=\"csrf-token\"]').getAttribute('content'), 'Content-Type'
|
||||
: 'application/json' , 'Accept' : 'application/json' }, body: JSON.stringify(this.pwdData) }); const
|
||||
data=await res.json(); if(res.ok && data.status === 'success') { alert(data.message);
|
||||
window.location.href = '{{ route('home') }}'; // 改密成功重新登录 } else {
|
||||
alert('密码修改失败: ' + (data.message || ' 请输入正确的旧密码')); } } catch (e) { alert('网络异常'); } finally {
|
||||
this.isSaving=false; } } }">
|
||||
<form @submit.prevent="savePassword">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">当前旧密码</label>
|
||||
<input type="password" x-model="pwdData.old_password" required
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-gray-500 focus:ring-gray-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">新密码</label>
|
||||
<input type="password" x-model="pwdData.new_password" required placeholder="至少 6 位字母或数字"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-gray-500 focus:ring-gray-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-bold text-gray-700 mb-2">确认新密码</label>
|
||||
<input type="password" x-model="pwdData.new_password_confirmation" required
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:border-gray-500 focus:ring-gray-500 p-2.5 bg-gray-50 border">
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 border-t pt-4">
|
||||
<button type="button" @click="showPasswordModal = false"
|
||||
class="px-4 py-2 bg-white border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 font-medium cursor-pointer">取消</button>
|
||||
<button type="submit" :disabled="isSaving"
|
||||
class="px-6 py-2 bg-gray-800 text-white rounded-md font-bold hover:bg-gray-900 shadow-sm disabled:opacity-50 cursor-pointer">确定修改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -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,
|
||||
// 这里可以视情况加入更多需要前端渲染在线人员列表的字段
|
||||
];
|
||||
});
|
||||
|
||||
@@ -1,7 +1,77 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\AuthController;
|
||||
use App\Http\Controllers\ChatController;
|
||||
use App\Http\Controllers\RoomController;
|
||||
use App\Http\Controllers\UserController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
// 聊天室首页 (即登录/注册页面)
|
||||
Route::get('/', function () {
|
||||
return view('welcome');
|
||||
return view('index'); // 指向 resources/views/index.blade.php
|
||||
})->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 查询
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user