feat: 实现挂机修仙、排行榜、大厅重构与全站留言板系统
- (Phase 8) 后台各维度管理与配置 - (Phase 9) 全自动静默挂机修仙升级 - (Phase 9) 四大维度风云排行榜页面 - (Phase 10) 全站留言板与悄悄话私信功能 - 运行 Pint 代码格式化
This commit is contained in:
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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', '系统参数已成功更新并生效!');
|
||||
}
|
||||
}
|
||||
@@ -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', '目标已被物理删除。');
|
||||
}
|
||||
}
|
||||
@@ -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' => '已成功退出。']);
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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', '该行留言已被抹除。');
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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}]。");
|
||||
}
|
||||
}
|
||||
@@ -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} 分钟。"]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user