filled('username')) { $query->where('username', 'like', '%'.$request->input('username').'%'); } // 从 Redis 获取所有在线用户名(跨所有房间去重) $onlineUsernames = collect(); foreach ($this->chatState->getAllActiveRoomIds() as $roomId) { $onlineUsernames = $onlineUsernames->merge(array_keys($this->chatState->getRoomUsers($roomId))); } $onlineUsernames = $onlineUsernames->unique()->values(); // 排序:允许的字段白名单,防止 SQL 注入 $sortable = ['user_level', 'exp_num', 'jjb', 'meili', 'id', 'online', 'wxid']; $sortBy = in_array($request->input('sort_by'), $sortable) ? $request->input('sort_by') : 'id'; $sortDir = $request->input('sort_dir') === 'asc' ? 'asc' : 'desc'; if ($sortBy === 'online') { // 用虚拟列排序:在线用户标记为 1,离线为 0;desc = 在线优先 if ($onlineUsernames->isNotEmpty()) { $placeholders = implode(',', array_fill(0, $onlineUsernames->count(), '?')); $query->orderByRaw( "CASE WHEN username IN ({$placeholders}) THEN 1 ELSE 0 END {$sortDir}", $onlineUsernames->toArray(), ); } $query->orderBy('id', 'desc'); // 二级排序 } else { $query->orderBy($sortBy, $sortDir); } $users = $query ->with(['activePosition.position.department', 'vipLevel']) ->paginate(20) ->withQueryString(); // VIP 等级选项列表(供编辑弹窗使用) $vipLevels = \App\Models\VipLevel::orderBy('sort_order')->get(); // 职务下拉选项(复用任命系统中的部门与职务数据) $departments = Department::with([ 'positions' => fn ($positionQuery) => $positionQuery->ordered(), ])->ordered()->get(); return view('admin.users.index', compact('users', 'vipLevels', 'departments', 'sortBy', 'sortDir', 'onlineUsernames')); } /** * 修改用户资料、等级或密码 (AJAX 或表单) * * @param User $user 路由模型自动注入 */ public function update(UpdateManagedUserRequest $request, User $user): JsonResponse|RedirectResponse { $targetUser = $user; $currentUser = Auth::user(); $responseMessages = []; // 超级管理员专属:仅 id=1 的账号可编辑用户信息 if ($currentUser->id !== 1) { if ($request->wantsJson()) { return response()->json(['status' => 'error', 'message' => '仅超级管理员(id=1)可编辑用户信息。'], 403); } abort(403, '仅超级管理员(id=1)可编辑用户信息。'); } // 越权防护:不能修改 等级大于或等于自己 的目标(除非修改自己) if ($targetUser->id !== $currentUser->id && $targetUser->user_level >= $currentUser->user_level) { return response()->json(['status' => 'error', 'message' => '权限不足:您无法修改同级或高级管理人员资料。'], 403); } $validated = $request->validated(); if (isset($validated['sex'])) { $targetUser->sex = $validated['sex']; } if (isset($validated['exp_num'])) { // 计算差值并通过统一服务记录流水(管理员手动调整) $expDiff = $validated['exp_num'] - ($targetUser->exp_num ?? 0); if ($expDiff !== 0) { $this->currencyService->change( $targetUser, 'exp', $expDiff, CurrencySource::ADMIN_ADJUST, "管理员 {$currentUser->username} 手动调整经验", ); $targetUser->refresh(); } // 调整经验后重新计算等级(有职务用户锁定职务等级,无职务用户按经验重算) $targetUser->load('activePosition.position'); $superLevel = (int) \App\Models\Sysparam::getValue('superlevel', '100'); if ($targetUser->activePosition?->position) { // 有在职职务:等级锁定为职务级,不受经验影响 $lockedLevel = (int) $targetUser->activePosition->position->level; if ($lockedLevel > 0 && $targetUser->user_level !== $lockedLevel) { $targetUser->user_level = $lockedLevel; } } elseif ($targetUser->user_level < $superLevel) { // 无职务普通用户:按经验重算等级(不超过满级阈值) $newLevel = \App\Models\Sysparam::calculateLevel($targetUser->exp_num ?? 0); $safeLevel = max(1, min($newLevel, $superLevel - 1)); $targetUser->user_level = $safeLevel; } } if (isset($validated['jjb'])) { $jjbDiff = $validated['jjb'] - ($targetUser->jjb ?? 0); if ($jjbDiff !== 0) { $this->currencyService->change( $targetUser, 'gold', $jjbDiff, CurrencySource::ADMIN_ADJUST, "管理员 {$currentUser->username} 手动调整金币", ); $targetUser->refresh(); } } if (isset($validated['meili'])) { $meiliDiff = $validated['meili'] - ($targetUser->meili ?? 0); if ($meiliDiff !== 0) { $this->currencyService->change( $targetUser, 'charm', $meiliDiff, CurrencySource::ADMIN_ADJUST, "管理员 {$currentUser->username} 手动调整魅力", ); $targetUser->refresh(); } } if (array_key_exists('qianming', $validated)) { $targetUser->qianming = $validated['qianming']; } if (isset($validated['headface'])) { $targetUser->headface = $validated['headface']; } // VIP 会员等级设置 if (array_key_exists('vip_level_id', $validated)) { $targetUser->vip_level_id = $validated['vip_level_id'] ?: null; } if (array_key_exists('hy_time', $validated)) { $targetUser->hy_time = $validated['hy_time'] ?: null; } if (! empty($validated['password'])) { $targetUser->password = Hash::make($validated['password']); } $targetUser->save(); if (array_key_exists('position_id', $validated)) { $positionSyncResult = $this->syncUserPosition( operator: $currentUser, targetUser: $targetUser, targetPositionId: $validated['position_id'], ); if (! $positionSyncResult['ok']) { return response()->json(['status' => 'error', 'message' => $positionSyncResult['message']], 422); } if (! empty($positionSyncResult['message'])) { $responseMessages[] = $positionSyncResult['message']; } } if ($request->wantsJson()) { $message = array_merge(['用户资料已强行更新完毕!'], $responseMessages); return response()->json(['status' => 'success', 'message' => implode(' ', $message)]); } $message = array_merge(['用户资料已更新!'], $responseMessages); return back()->with('success', implode(' ', $message)); } /** * 物理删除杀封用户 * * @param User $user 路由模型自动注入 */ public function destroy(Request $request, User $user): RedirectResponse { $targetUser = $user; $currentUser = Auth::user(); // 超级管理员专属:仅 id=1 的账号可删除用户 if ($currentUser->id !== 1) { abort(403, '仅超级管理员(id=1)可删除用户。'); } // 越权防护:不允许删除同级或更高等级的账号 if ($targetUser->id !== $currentUser->id && $targetUser->user_level >= $currentUser->user_level) { abort(403, '权限不足:无法删除同级或高级账号!'); } // 管理员保护:达到踢人等级(level_kick)的用户视为管理员,不可被强杀 $levelKick = (int) \App\Models\Sysparam::getValue('level_kick', '10'); if ($targetUser->user_level >= $levelKick) { abort(403, '该用户为管理员,不允许强杀!请先在用户编辑中降低其等级。'); } $targetUser->delete(); return back()->with('success', '目标已被物理删除。'); } /** * 方法功能:同步后台编辑页选择的目标职务。 * * @return array{ok: bool, message: string} */ private function syncUserPosition(User $operator, User $targetUser, ?int $targetPositionId): array { $currentAssignment = $this->appointmentService->getActivePosition($targetUser); $currentPositionId = $currentAssignment?->position_id; if ($targetPositionId === $currentPositionId) { return ['ok' => true, 'message' => '']; } if ($targetPositionId === null) { if (! $currentAssignment) { return ['ok' => true, 'message' => '']; } $result = $this->appointmentService->revoke($operator, $targetUser, '后台用户管理编辑'); if (! $result['ok']) { return $result; } $this->broadcastRevokedPosition($operator, $targetUser, $currentAssignment); return ['ok' => true, 'message' => '用户职务已撤销。']; } $targetPosition = Position::with('department')->findOrFail($targetPositionId); if ($currentAssignment) { $revokeResult = $this->appointmentService->revoke($operator, $targetUser, '后台用户管理编辑'); if (! $revokeResult['ok']) { return $revokeResult; } } $appointResult = $this->appointmentService->appoint($operator, $targetUser, $targetPosition, '后台用户管理编辑'); if (! $appointResult['ok']) { return $appointResult; } $this->broadcastAppointedPosition($operator, $targetUser, $targetPosition); return ['ok' => true, 'message' => "用户职务已更新为【{$targetPosition->name}】。"]; } /** * 方法功能:广播后台任命成功后的公告与目标用户刷新事件。 */ private function broadcastAppointedPosition(User $operator, User $targetUser, Position $targetPosition): void { foreach ($this->chatState->getAllActiveRoomIds() as $roomId) { broadcast(new AppointmentAnnounced( roomId: $roomId, targetUsername: $targetUser->username, positionIcon: $targetPosition->icon ?? '🎖️', positionName: $targetPosition->name, departmentName: $targetPosition->department?->name ?? '', operatorName: $operator->username, )); } broadcast(new UserBrowserRefreshRequested( targetUserId: $targetUser->id, operator: $operator->username, reason: '你的职务已发生变更,页面权限正在同步更新。', )); } /** * 方法功能:广播后台撤销职务后的公告与目标用户刷新事件。 */ private function broadcastRevokedPosition(User $operator, User $targetUser, UserPosition $currentAssignment): void { $currentAssignment->loadMissing('position.department'); foreach ($this->chatState->getAllActiveRoomIds() as $roomId) { broadcast(new AppointmentAnnounced( roomId: $roomId, targetUsername: $targetUser->username, positionIcon: $currentAssignment->position?->icon ?? '🎖️', positionName: $currentAssignment->position?->name ?? '', departmentName: $currentAssignment->position?->department?->name ?? '', operatorName: $operator->username, type: 'revoke', )); } broadcast(new UserBrowserRefreshRequested( targetUserId: $targetUser->id, operator: $operator->username, reason: '你的职务已被撤销,页面权限正在同步更新。', )); } }