feat: 任命/撤销通知系统 + 用户名片UI优化
- 任命/撤销事件增加 type 字段区分类型 - 任命:全屏礼花 + 紫色弹窗 + 紫色系统消息 - 撤销:灰色弹窗 + 灰色系统消息,无礼花 - 消息分发:操作者/被操作者显示在私聊面板,其他人显示在公屏 - 系统消息加随机鼓励语(各5条轮换) - ChatStateService 修复 Redis key 前缀扫描问题(getAllActiveRoomIds) - 用户名片折叠优化:管理员视野、职务履历均可折叠 - 管理操作 + 职务操作合并为「🔧 管理操作」折叠区 - 悄悄话改为「🎁 送礼物」按钮,礼物面板内联展开
This commit is contained in:
202
app/Http/Controllers/Admin/AppointmentController.php
Normal file
202
app/Http/Controllers/Admin/AppointmentController.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台任命管理控制器
|
||||
* 管理员可以在此查看所有在职人员、进行新增任命和撤销职务
|
||||
* 任命/撤销通过 AppointmentService 执行,权限日志自动记录
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Events\AppointmentAnnounced;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Department;
|
||||
use App\Models\Position;
|
||||
use App\Models\User;
|
||||
use App\Models\UserPosition;
|
||||
use App\Services\AppointmentService;
|
||||
use App\Services\ChatStateService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AppointmentController extends Controller
|
||||
{
|
||||
/**
|
||||
* 注入任命服务
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly AppointmentService $appointmentService,
|
||||
private readonly ChatStateService $chatState,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 任命管理主列表(当前全部在职人员)
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
// 所有在职记录(按部门 rank 倒序、职务 rank 倒序)
|
||||
$activePositions = UserPosition::query()
|
||||
->where('is_active', true)
|
||||
->with([
|
||||
'user',
|
||||
'position.department',
|
||||
'appointedBy',
|
||||
])
|
||||
->join('positions', 'user_positions.position_id', '=', 'positions.id')
|
||||
->join('departments', 'positions.department_id', '=', 'departments.id')
|
||||
->orderByDesc('departments.rank')
|
||||
->orderByDesc('positions.rank')
|
||||
->select('user_positions.*')
|
||||
->get();
|
||||
|
||||
// 部门+职务(供新增任命弹窗下拉选择,带在职人数统计)
|
||||
$departments = Department::with([
|
||||
'positions' => fn ($q) => $q->withCount('activeUserPositions')->ordered(),
|
||||
])->ordered()->get();
|
||||
|
||||
return view('admin.appointments.index', compact('activePositions', 'departments'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行新增任命
|
||||
* 管理员在后台直接任命用户,操作人为当前登录管理员
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$request->validate([
|
||||
'username' => 'required|string|exists:users,username',
|
||||
'position_id' => 'required|exists:positions,id',
|
||||
'remark' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$operator = Auth::user();
|
||||
$target = User::where('username', $request->username)->firstOrFail();
|
||||
$targetPosition = Position::with('department')->findOrFail($request->position_id);
|
||||
|
||||
$result = $this->appointmentService->appoint($operator, $target, $targetPosition, $request->remark);
|
||||
|
||||
if ($result['ok']) {
|
||||
// 向所有当前有人在线的聊天室广播礼花公告(后台操作人不在聊天室内)
|
||||
foreach ($this->chatState->getAllActiveRoomIds() as $roomId) {
|
||||
broadcast(new AppointmentAnnounced(
|
||||
roomId: $roomId,
|
||||
targetUsername: $target->username,
|
||||
positionIcon: $targetPosition->icon ?? '🎖️',
|
||||
positionName: $targetPosition->name,
|
||||
departmentName: $targetPosition->department?->name ?? '',
|
||||
operatorName: $operator->username,
|
||||
));
|
||||
}
|
||||
|
||||
return redirect()->route('admin.appointments.index')->with('success', $result['message']);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.appointments.index')->with('error', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销职务
|
||||
*/
|
||||
public function revoke(Request $request, UserPosition $userPosition): RedirectResponse
|
||||
{
|
||||
$operator = Auth::user();
|
||||
$target = $userPosition->user;
|
||||
|
||||
// 撤销前先记录职务信息(撤销后关联就断了)
|
||||
$userPosition->load('position.department');
|
||||
$posIcon = $userPosition->position?->icon ?? '🎖️';
|
||||
$posName = $userPosition->position?->name ?? '';
|
||||
$deptName = $userPosition->position?->department?->name ?? '';
|
||||
|
||||
$result = $this->appointmentService->revoke($operator, $target, $request->remark);
|
||||
|
||||
if ($result['ok']) {
|
||||
// 向所有活跃房间广播撤销公告
|
||||
if ($posName) {
|
||||
foreach ($this->chatState->getAllActiveRoomIds() as $roomId) {
|
||||
broadcast(new AppointmentAnnounced(
|
||||
roomId: $roomId,
|
||||
targetUsername: $target->username,
|
||||
positionIcon: $posIcon,
|
||||
positionName: $posName,
|
||||
departmentName: $deptName,
|
||||
operatorName: $operator->username,
|
||||
type: 'revoke',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('admin.appointments.index')->with('success', $result['message']);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.appointments.index')->with('error', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看某任职记录的在职登录日志
|
||||
*/
|
||||
public function dutyLogs(UserPosition $userPosition): View
|
||||
{
|
||||
$userPosition->load(['user', 'position.department', 'appointedBy']);
|
||||
|
||||
$logs = $userPosition->dutyLogs()
|
||||
->orderByDesc('login_at')
|
||||
->paginate(30);
|
||||
|
||||
return view('admin.appointments.duty-logs', compact('userPosition', 'logs'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查看某任职记录的权限操作日志
|
||||
*/
|
||||
public function authorityLogs(UserPosition $userPosition): View
|
||||
{
|
||||
$userPosition->load(['user', 'position.department']);
|
||||
|
||||
$logs = $userPosition->authorityLogs()
|
||||
->with(['targetUser', 'targetPosition'])
|
||||
->orderByDesc('created_at')
|
||||
->paginate(30);
|
||||
|
||||
return view('admin.appointments.authority-logs', compact('userPosition', 'logs'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 历史任职记录(全部 is_active=false 的记录)
|
||||
*/
|
||||
public function history(): View
|
||||
{
|
||||
$history = UserPosition::query()
|
||||
->where('is_active', false)
|
||||
->with(['user', 'position.department', 'appointedBy', 'revokedBy'])
|
||||
->orderByDesc('revoked_at')
|
||||
->paginate(30);
|
||||
|
||||
return view('admin.appointments.history', compact('history'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索用户(供任命弹窗 Ajax 快速查找)
|
||||
*/
|
||||
public function searchUsers(Request $request): JsonResponse
|
||||
{
|
||||
$keyword = $request->input('q', '');
|
||||
|
||||
$users = User::query()
|
||||
->where('id', '!=', 1) // 排除超级管理员
|
||||
->where('username', 'like', "%{$keyword}%")
|
||||
->whereDoesntHave('activePosition') // 排除已有职务的用户
|
||||
->select('id', 'username', 'user_level')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
return response()->json($users);
|
||||
}
|
||||
}
|
||||
@@ -52,10 +52,12 @@ class AutoactController extends Controller
|
||||
|
||||
/**
|
||||
* 更新事件
|
||||
*
|
||||
* @param Autoact $autoact 路由模型自动注入
|
||||
*/
|
||||
public function update(Request $request, int $id): RedirectResponse
|
||||
public function update(Request $request, Autoact $autoact): RedirectResponse
|
||||
{
|
||||
$event = Autoact::findOrFail($id);
|
||||
$event = $autoact;
|
||||
|
||||
$data = $request->validate([
|
||||
'text_body' => 'required|string|max:500',
|
||||
@@ -71,26 +73,29 @@ class AutoactController extends Controller
|
||||
|
||||
/**
|
||||
* 切换事件启用/禁用状态
|
||||
*
|
||||
* @param Autoact $autoact 路由模型自动注入
|
||||
*/
|
||||
public function toggle(int $id): JsonResponse
|
||||
public function toggle(Autoact $autoact): JsonResponse
|
||||
{
|
||||
$event = Autoact::findOrFail($id);
|
||||
$event->enabled = ! $event->enabled;
|
||||
$event->save();
|
||||
$autoact->enabled = ! $autoact->enabled;
|
||||
$autoact->save();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'enabled' => $event->enabled,
|
||||
'message' => $event->enabled ? '已启用' : '已禁用',
|
||||
'enabled' => $autoact->enabled,
|
||||
'message' => $autoact->enabled ? '已启用' : '已禁用',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除事件
|
||||
*
|
||||
* @param Autoact $autoact 路由模型自动注入
|
||||
*/
|
||||
public function destroy(int $id): RedirectResponse
|
||||
public function destroy(Autoact $autoact): RedirectResponse
|
||||
{
|
||||
Autoact::findOrFail($id)->delete();
|
||||
$autoact->delete();
|
||||
|
||||
return redirect()->route('admin.autoact.index')->with('success', '事件已删除!');
|
||||
}
|
||||
|
||||
191
app/Http/Controllers/Admin/ChangelogController.php
Normal file
191
app/Http/Controllers/Admin/ChangelogController.php
Normal file
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台开发日志管理控制器(仅 id=1 超级管理员可访问)
|
||||
* 提供开发日志的 CRUD 功能,发布时可选择向 Room 1 大厅广播通知
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Events\ChangelogPublished;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\SaveMessageJob;
|
||||
use App\Models\DevChangelog;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ChangelogController extends Controller
|
||||
{
|
||||
/**
|
||||
* 后台日志列表(含草稿)
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$logs = DevChangelog::orderByDesc('created_at')->paginate(20);
|
||||
|
||||
return view('admin.changelog.index', compact('logs'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增日志表单页(预填今日日期)
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
// 预填今日日期为版本号
|
||||
$todayVersion = now()->format('Y-m-d');
|
||||
|
||||
return view('admin.changelog.form', [
|
||||
'log' => null,
|
||||
'todayVersion' => $todayVersion,
|
||||
'typeOptions' => DevChangelog::TYPE_CONFIG,
|
||||
'isCreate' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存新日志
|
||||
* 若勾选"立即发布",则记录 published_at 并可选向大厅广播通知
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'version' => 'required|string|max:30',
|
||||
'title' => 'required|string|max:200',
|
||||
'type' => 'required|in:feature,fix,improve,other',
|
||||
'content' => 'required|string',
|
||||
'is_published' => 'nullable|boolean',
|
||||
'notify_chat' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$isPublished = (bool) ($data['is_published'] ?? false);
|
||||
$notifyChat = (bool) ($data['notify_chat'] ?? false);
|
||||
|
||||
/** @var DevChangelog $log */
|
||||
$log = DevChangelog::create([
|
||||
'version' => $data['version'],
|
||||
'title' => $data['title'],
|
||||
'type' => $data['type'],
|
||||
'content' => $data['content'],
|
||||
'is_published' => $isPublished,
|
||||
// 只有同时勾选"通知"才记录 notify_chat,否则置 false
|
||||
'notify_chat' => $isPublished && $notifyChat,
|
||||
// 首次发布时记录发布时间
|
||||
'published_at' => $isPublished ? Carbon::now() : null,
|
||||
]);
|
||||
|
||||
// 如果发布且勾选了"通知大厅用户",则触发 WebSocket 广播 + 持久化到消息库
|
||||
if ($isPublished && $notifyChat) {
|
||||
event(new ChangelogPublished($log));
|
||||
$this->saveChangelogNotification($log);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.changelogs.index')
|
||||
->with('success', '开发日志创建成功!'.($isPublished ? '(已发布)' : '(草稿已保存)'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑日志表单页
|
||||
*
|
||||
* @param DevChangelog $changelog 路由模型自动注入
|
||||
*/
|
||||
public function edit(DevChangelog $changelog): View
|
||||
{
|
||||
return view('admin.changelog.form', [
|
||||
'log' => $changelog,
|
||||
'todayVersion' => $changelog->version,
|
||||
'typeOptions' => DevChangelog::TYPE_CONFIG,
|
||||
'isCreate' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新日志内容(编辑操作不更新 published_at,不触发通知)
|
||||
*
|
||||
* @param DevChangelog $changelog 路由模型自动注入
|
||||
*/
|
||||
public function update(Request $request, DevChangelog $changelog): RedirectResponse
|
||||
{
|
||||
$log = $changelog;
|
||||
|
||||
$data = $request->validate([
|
||||
'version' => 'required|string|max:30',
|
||||
'title' => 'required|string|max:200',
|
||||
'type' => 'required|in:feature,fix,improve,other',
|
||||
'content' => 'required|string',
|
||||
'is_published' => 'nullable|boolean',
|
||||
'notify_chat' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$isPublished = (bool) ($data['is_published'] ?? false);
|
||||
|
||||
// 如果从草稿切换为发布,记录首次发布时间
|
||||
$publishedAt = $log->published_at;
|
||||
if ($isPublished && ! $log->is_published) {
|
||||
$publishedAt = Carbon::now();
|
||||
} elseif (! $isPublished) {
|
||||
// 从发布退回草稿,清除发布时间
|
||||
$publishedAt = null;
|
||||
}
|
||||
|
||||
$log->update([
|
||||
'version' => $data['version'],
|
||||
'title' => $data['title'],
|
||||
'type' => $data['type'],
|
||||
'content' => $data['content'],
|
||||
'is_published' => $isPublished,
|
||||
'published_at' => $publishedAt,
|
||||
]);
|
||||
|
||||
// 若勾选了「通知大厅用户」且当前已发布,则广播通知 + 持久化到消息库
|
||||
$notifyChat = (bool) ($data['notify_chat'] ?? false);
|
||||
if ($notifyChat && $isPublished) {
|
||||
event(new ChangelogPublished($log));
|
||||
$this->saveChangelogNotification($log);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.changelogs.index')
|
||||
->with('success', '开发日志已更新!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除日志
|
||||
*
|
||||
* @param DevChangelog $changelog 路由模型自动注入
|
||||
*/
|
||||
public function destroy(DevChangelog $changelog): RedirectResponse
|
||||
{
|
||||
$changelog->delete();
|
||||
|
||||
return redirect()->route('admin.changelogs.index')
|
||||
->with('success', '日志已删除。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 将版本更新通知持久化为 Room 1 系统消息
|
||||
* 确保用户重进聊天室时仍能在历史消息中看到该通知
|
||||
*
|
||||
* @param DevChangelog $log 已发布的日志
|
||||
*/
|
||||
private function saveChangelogNotification(DevChangelog $log): void
|
||||
{
|
||||
$typeLabel = DevChangelog::TYPE_CONFIG[$log->type]['label'] ?? '更新';
|
||||
$url = url('/changelog').'#v'.$log->version;
|
||||
|
||||
SaveMessageJob::dispatch([
|
||||
'room_id' => 1,
|
||||
'from_user' => '系统公告',
|
||||
'to_user' => '大家',
|
||||
'content' => "📢 【版本更新 {$typeLabel}】v{$log->version}《{$log->title}》— <a href=\"{$url}\" target=\"_blank\" class=\"underline\">点击查看详情</a>",
|
||||
'is_secret' => false,
|
||||
'font_color' => '#7c3aed',
|
||||
'action' => '',
|
||||
'sent_at' => now()->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
91
app/Http/Controllers/Admin/DepartmentController.php
Normal file
91
app/Http/Controllers/Admin/DepartmentController.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台部门管理控制器
|
||||
* 提供部门的 CRUD 功能(增删改查)
|
||||
* 部门是职务的上级分类,包含位阶、颜色、描述等配置
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Department;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DepartmentController extends Controller
|
||||
{
|
||||
/**
|
||||
* 部门列表页
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$departments = Department::withCount(['positions'])
|
||||
->orderBy('sort_order')
|
||||
->orderByDesc('rank')
|
||||
->get();
|
||||
|
||||
return view('admin.departments.index', compact('departments'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建部门
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:50|unique:departments,name',
|
||||
'rank' => 'required|integer|min:0|max:99',
|
||||
'color' => 'required|string|max:10',
|
||||
'sort_order' => 'required|integer|min:0',
|
||||
'description' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
Department::create($data);
|
||||
|
||||
return redirect()->route('admin.departments.index')->with('success', "部门【{$data['name']}】创建成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新部门
|
||||
*/
|
||||
public function update(Request $request, Department $department): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:50|unique:departments,name,'.$department->id,
|
||||
'rank' => 'required|integer|min:0|max:99',
|
||||
'color' => 'required|string|max:10',
|
||||
'sort_order' => 'required|integer|min:0',
|
||||
'description' => 'nullable|string|max:255',
|
||||
]);
|
||||
|
||||
$department->update($data);
|
||||
|
||||
return redirect()->route('admin.departments.index')->with('success', "部门【{$data['name']}】更新成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除部门(级联删除职务)
|
||||
*/
|
||||
public function destroy(Department $department): RedirectResponse
|
||||
{
|
||||
// 检查是否有在职人员
|
||||
$activeMemberCount = $department->positions()
|
||||
->whereHas('activeUserPositions')
|
||||
->count();
|
||||
|
||||
if ($activeMemberCount > 0) {
|
||||
return redirect()->route('admin.departments.index')
|
||||
->with('error', "部门【{$department->name}】下尚有在职人员,请先撤销所有职务后再删除。");
|
||||
}
|
||||
|
||||
$department->delete();
|
||||
|
||||
return redirect()->route('admin.departments.index')->with('success', "部门【{$department->name}】已删除!");
|
||||
}
|
||||
}
|
||||
112
app/Http/Controllers/Admin/FeedbackManagerController.php
Normal file
112
app/Http/Controllers/Admin/FeedbackManagerController.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台用户反馈管理控制器(仅 id=1 超级管理员可访问)
|
||||
* 提供反馈列表查看、处理状态修改、官方回复功能
|
||||
* 侧边栏显示待处理数量徽标
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\FeedbackItem;
|
||||
use App\Models\FeedbackReply;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class FeedbackManagerController extends Controller
|
||||
{
|
||||
/**
|
||||
* 后台反馈列表页(支持类型+状态筛选)
|
||||
*
|
||||
* @param Request $request 含 type/status 筛选参数
|
||||
*/
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$type = $request->input('type');
|
||||
$status = $request->input('status');
|
||||
|
||||
$query = FeedbackItem::with(['replies'])
|
||||
->orderByDesc('created_at');
|
||||
|
||||
// 按类型筛选
|
||||
if ($type && in_array($type, ['bug', 'suggestion'])) {
|
||||
$query->ofType($type);
|
||||
}
|
||||
|
||||
// 按状态筛选
|
||||
if ($status && array_key_exists($status, FeedbackItem::STATUS_CONFIG)) {
|
||||
$query->ofStatus($status);
|
||||
}
|
||||
|
||||
$feedbacks = $query->paginate(20)->withQueryString();
|
||||
|
||||
// 待处理数量(用于侧边栏徽标)
|
||||
$pendingCount = FeedbackItem::pending()->count();
|
||||
|
||||
return view('admin.feedback.index', [
|
||||
'feedbacks' => $feedbacks,
|
||||
'pendingCount' => $pendingCount,
|
||||
'statusConfig' => FeedbackItem::STATUS_CONFIG,
|
||||
'typeConfig' => FeedbackItem::TYPE_CONFIG,
|
||||
'currentType' => $type,
|
||||
'currentStatus' => $status,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新反馈处理状态和官方回复(Ajax + 表单双模式)
|
||||
* Ajax 返回 JSON,普通表单提交返回重定向
|
||||
*
|
||||
* @param Request $request 含 status/admin_remark 字段
|
||||
* @param int $id 反馈 ID
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse|RedirectResponse
|
||||
{
|
||||
$feedback = FeedbackItem::findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'status' => 'required|in:'.implode(',', array_keys(FeedbackItem::STATUS_CONFIG)),
|
||||
'admin_remark' => 'nullable|string|max:2000',
|
||||
]);
|
||||
|
||||
$feedback->update([
|
||||
'status' => $data['status'],
|
||||
'admin_remark' => $data['admin_remark'] ?? $feedback->admin_remark,
|
||||
]);
|
||||
|
||||
// 如果有新的官方回复内容,同时写入 feedback_replies(带 is_admin 标记)
|
||||
if (! empty($data['admin_remark']) && $data['admin_remark'] !== $feedback->getOriginal('admin_remark')) {
|
||||
DB::transaction(function () use ($feedback, $data): void {
|
||||
FeedbackReply::create([
|
||||
'feedback_id' => $feedback->id,
|
||||
'user_id' => 1,
|
||||
'username' => '🛡️ 开发者',
|
||||
'content' => $data['admin_remark'],
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$feedback->increment('replies_count');
|
||||
});
|
||||
}
|
||||
|
||||
// Ajax 请求返回 JSON
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'new_status' => $data['status'],
|
||||
'status_label' => FeedbackItem::STATUS_CONFIG[$data['status']]['icon'].' '.FeedbackItem::STATUS_CONFIG[$data['status']]['label'],
|
||||
'status_color' => FeedbackItem::STATUS_CONFIG[$data['status']]['color'],
|
||||
]);
|
||||
}
|
||||
|
||||
return redirect()->route('admin.feedback.index')
|
||||
->with('success', '反馈状态已更新!');
|
||||
}
|
||||
}
|
||||
110
app/Http/Controllers/Admin/PositionController.php
Normal file
110
app/Http/Controllers/Admin/PositionController.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:后台职务管理控制器
|
||||
* 提供职务的 CRUD 功能,包含任命权限白名单(多选 position_appoint_limits)的同步
|
||||
* 职务属于部门,包含等级、图标、人数上限、奖励上限等配置
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Department;
|
||||
use App\Models\Position;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class PositionController extends Controller
|
||||
{
|
||||
/**
|
||||
* 职务列表页
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
// 按部门分组展示
|
||||
$departments = Department::with([
|
||||
'positions' => fn ($q) => $q->withCount(['activeUserPositions'])->ordered(),
|
||||
])->ordered()->get();
|
||||
|
||||
// 全部职务(供任命白名单多选框使用)
|
||||
$allPositions = Position::with('department')->orderByDesc('rank')->get();
|
||||
|
||||
return view('admin.positions.index', compact('departments', 'allPositions'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建职务(同时同步任命白名单)
|
||||
*/
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'department_id' => 'required|exists:departments,id',
|
||||
'name' => 'required|string|max:50',
|
||||
'icon' => 'nullable|string|max:10',
|
||||
'rank' => 'required|integer|min:0|max:99',
|
||||
'level' => 'required|integer|min:1|max:100',
|
||||
'max_persons' => 'nullable|integer|min:1',
|
||||
'max_reward' => 'nullable|integer|min:0',
|
||||
'sort_order' => 'required|integer|min:0',
|
||||
'appointable_ids' => 'nullable|array',
|
||||
'appointable_ids.*' => 'exists:positions,id',
|
||||
]);
|
||||
|
||||
$appointableIds = $data['appointable_ids'] ?? [];
|
||||
unset($data['appointable_ids']);
|
||||
|
||||
$position = Position::create($data);
|
||||
|
||||
// 同步任命白名单(有选则写,没选则清空=无任命权)
|
||||
$position->appointablePositions()->sync($appointableIds);
|
||||
|
||||
return redirect()->route('admin.positions.index')->with('success', "职务【{$data['name']}】创建成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新职务(含任命白名单同步)
|
||||
*/
|
||||
public function update(Request $request, Position $position): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'department_id' => 'required|exists:departments,id',
|
||||
'name' => 'required|string|max:50',
|
||||
'icon' => 'nullable|string|max:10',
|
||||
'rank' => 'required|integer|min:0|max:99',
|
||||
'level' => 'required|integer|min:1|max:100',
|
||||
'max_persons' => 'nullable|integer|min:1',
|
||||
'max_reward' => 'nullable|integer|min:0',
|
||||
'sort_order' => 'required|integer|min:0',
|
||||
'appointable_ids' => 'nullable|array',
|
||||
'appointable_ids.*' => 'exists:positions,id',
|
||||
]);
|
||||
|
||||
$appointableIds = $data['appointable_ids'] ?? [];
|
||||
unset($data['appointable_ids']);
|
||||
|
||||
$position->update($data);
|
||||
$position->appointablePositions()->sync($appointableIds);
|
||||
|
||||
return redirect()->route('admin.positions.index')->with('success', "职务【{$data['name']}】更新成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除职务(有在职人员时拒绝)
|
||||
*/
|
||||
public function destroy(Position $position): RedirectResponse
|
||||
{
|
||||
if ($position->isFull() || $position->activeUserPositions()->exists()) {
|
||||
return redirect()->route('admin.positions.index')
|
||||
->with('error', "职务【{$position->name}】尚有在职人员,请先撤销后再删除。");
|
||||
}
|
||||
|
||||
$position->delete();
|
||||
|
||||
return redirect()->route('admin.positions.index')->with('success', "职务【{$position->name}】已删除!");
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,11 @@ class RoomManagerController extends Controller
|
||||
|
||||
/**
|
||||
* 更新房间信息
|
||||
*
|
||||
* @param Room $room 路由模型自动注入
|
||||
*/
|
||||
public function update(Request $request, int $id): RedirectResponse
|
||||
public function update(Request $request, Room $room): RedirectResponse
|
||||
{
|
||||
$room = Room::findOrFail($id);
|
||||
|
||||
$data = $request->validate([
|
||||
'room_name' => 'required|string|max:100',
|
||||
'room_des' => 'nullable|string|max:500',
|
||||
@@ -52,11 +52,11 @@ class RoomManagerController extends Controller
|
||||
|
||||
/**
|
||||
* 删除房间(非系统房间)
|
||||
*
|
||||
* @param Room $room 路由模型自动注入
|
||||
*/
|
||||
public function destroy(int $id): RedirectResponse
|
||||
public function destroy(Room $room): RedirectResponse
|
||||
{
|
||||
$room = Room::findOrFail($id);
|
||||
|
||||
if ($room->room_keep) {
|
||||
return redirect()->route('admin.rooms.index')->with('error', '系统房间不允许删除!');
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Enums\CurrencySource;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Services\UserCurrencyService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -30,6 +30,7 @@ class UserManagerController extends Controller
|
||||
public function __construct(
|
||||
private readonly UserCurrencyService $currencyService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 显示用户列表及搜索(支持按等级/经验/金币/魅力排序)
|
||||
*/
|
||||
@@ -42,11 +43,15 @@ class UserManagerController extends Controller
|
||||
}
|
||||
|
||||
// 排序:允许的字段白名单,防止 SQL 注入
|
||||
$sortable = ['user_level', 'exp_num', 'jjb', 'meili', 'id'];
|
||||
$sortBy = in_array($request->input('sort_by'), $sortable) ? $request->input('sort_by') : 'id';
|
||||
$sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc';
|
||||
$sortable = ['user_level', 'exp_num', 'jjb', 'meili', 'id'];
|
||||
$sortBy = in_array($request->input('sort_by'), $sortable) ? $request->input('sort_by') : 'id';
|
||||
$sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc';
|
||||
|
||||
$users = $query->orderBy($sortBy, $sortDir)->paginate(20)->withQueryString();
|
||||
$users = $query
|
||||
->with(['activePosition.position.department', 'vipLevel'])
|
||||
->orderBy($sortBy, $sortDir)
|
||||
->paginate(20)
|
||||
->withQueryString();
|
||||
|
||||
// VIP 等级选项列表(供编辑弹窗使用)
|
||||
$vipLevels = \App\Models\VipLevel::orderBy('sort_order')->get();
|
||||
@@ -56,10 +61,12 @@ class UserManagerController extends Controller
|
||||
|
||||
/**
|
||||
* 修改用户资料、等级或密码 (AJAX 或表单)
|
||||
*
|
||||
* @param User $user 路由模型自动注入
|
||||
*/
|
||||
public function update(Request $request, int $id): JsonResponse|RedirectResponse
|
||||
public function update(Request $request, User $user): JsonResponse|RedirectResponse
|
||||
{
|
||||
$targetUser = User::findOrFail($id);
|
||||
$targetUser = $user;
|
||||
$currentUser = Auth::user();
|
||||
|
||||
// 越权防护:不能修改 等级大于或等于自己 的目标(除非修改自己)
|
||||
@@ -67,12 +74,8 @@ class UserManagerController extends Controller
|
||||
return response()->json(['status' => 'error', 'message' => '权限不足:您无法修改同级或高级管理人员资料。'], 403);
|
||||
}
|
||||
|
||||
// 管理员级别 = 最高等级 + 1,后台编辑最高可设到管理员级别
|
||||
$adminLevel = (int) \App\Models\Sysparam::getValue('maxlevel', '15') + 1;
|
||||
|
||||
$validated = $request->validate([
|
||||
'sex' => 'sometimes|integer|in:0,1,2',
|
||||
'user_level' => "sometimes|integer|min:0|max:{$adminLevel}",
|
||||
'exp_num' => 'sometimes|integer|min:0',
|
||||
'jjb' => 'sometimes|integer|min:0',
|
||||
'meili' => 'sometimes|integer|min:0',
|
||||
@@ -83,17 +86,6 @@ class UserManagerController extends Controller
|
||||
'hy_time' => 'sometimes|nullable|date',
|
||||
]);
|
||||
|
||||
// 如果传了且没超权,直接赋予
|
||||
if (isset($validated['user_level'])) {
|
||||
if ($currentUser->id !== $targetUser->id) {
|
||||
// 修改别人:只有真正的创始人 (ID=1) 才能修改别人的等级
|
||||
if ($currentUser->id !== 1) {
|
||||
return response()->json(['status' => 'error', 'message' => '权限越界:只有星系创始人(站长)才能调整其他用户的行政等级!'], 403);
|
||||
}
|
||||
}
|
||||
$targetUser->user_level = $validated['user_level'];
|
||||
}
|
||||
|
||||
if (isset($validated['sex'])) {
|
||||
$targetUser->sex = $validated['sex'];
|
||||
}
|
||||
@@ -158,10 +150,12 @@ class UserManagerController extends Controller
|
||||
|
||||
/**
|
||||
* 物理删除杀封用户
|
||||
*
|
||||
* @param User $user 路由模型自动注入
|
||||
*/
|
||||
public function destroy(Request $request, int $id): RedirectResponse
|
||||
public function destroy(Request $request, User $user): RedirectResponse
|
||||
{
|
||||
$targetUser = User::findOrFail($id);
|
||||
$targetUser = $user;
|
||||
$currentUser = Auth::user();
|
||||
|
||||
// 越权防护:不允许删除同级或更高等级的账号
|
||||
|
||||
@@ -60,11 +60,11 @@ class VipController extends Controller
|
||||
/**
|
||||
* 更新会员等级
|
||||
*
|
||||
* @param int $id 等级ID
|
||||
* @param VipLevel $vip 路由模型自动注入
|
||||
*/
|
||||
public function update(Request $request, int $id): RedirectResponse
|
||||
public function update(Request $request, VipLevel $vip): RedirectResponse
|
||||
{
|
||||
$level = VipLevel::findOrFail($id);
|
||||
$level = $vip;
|
||||
|
||||
$data = $request->validate([
|
||||
'name' => 'required|string|max:50',
|
||||
@@ -90,12 +90,11 @@ class VipController extends Controller
|
||||
/**
|
||||
* 删除会员等级(关联用户的 vip_level_id 会自动置 null)
|
||||
*
|
||||
* @param int $id 等级ID
|
||||
* @param VipLevel $vip 路由模型自动注入
|
||||
*/
|
||||
public function destroy(int $id): RedirectResponse
|
||||
public function destroy(VipLevel $vip): RedirectResponse
|
||||
{
|
||||
$level = VipLevel::findOrFail($id);
|
||||
$level->delete();
|
||||
$vip->delete();
|
||||
|
||||
return redirect()->route('admin.vip.index')->with('success', '会员等级已删除!');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user