diff --git a/app/Http/Controllers/AdminCommandController.php b/app/Http/Controllers/AdminCommandController.php index d159159..406e891 100644 --- a/app/Http/Controllers/AdminCommandController.php +++ b/app/Http/Controllers/AdminCommandController.php @@ -68,10 +68,12 @@ class AdminCommandController extends Controller $targetUsername = $request->input('username'); $roomId = $request->input('room_id'); $reason = $request->input('reason', '请注意言行'); + $operatorDisplay = $this->buildOperatorDisplayHtml($admin); - // 权限检查(等级由 level_warn 配置) - if (! $this->canExecute($admin, $targetUsername, 'level_warn', '5')) { - return response()->json(['status' => 'error', 'message' => '权限不足'], 403); + // 权限检查:必须拥有职务权限,且不能处理职务高于自己的用户。 + $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_WARN, '警告'); + if (! $authorization['ok']) { + return response()->json(['status' => 'error', 'message' => $authorization['message']], 403); } // 广播警告消息 @@ -80,7 +82,7 @@ class AdminCommandController extends Controller 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', - 'content' => "⚠️ 管理员 {$admin->username} 警告 {$targetUsername}:{$reason}", + 'content' => "⚠️ {$operatorDisplay} 警告 {$targetUsername}:{$reason}", 'is_secret' => false, 'font_color' => '#dc2626', 'action' => '', @@ -94,9 +96,9 @@ class AdminCommandController extends Controller $this->pushTargetToastMessage( roomId: (int) $roomId, targetUsername: $targetUsername, - content: "⚠️ 管理员 {$admin->username} 警告了你:{$reason}", + content: "⚠️ {$operatorDisplay} 警告了你:{$reason}", title: '⚠️ 收到警告', - toastMessage: "{$admin->username} 警告了你:{$reason}", + toastMessage: "{$operatorDisplay} 警告了你:{$reason}", color: '#f59e0b', icon: '⚠️', ); @@ -124,19 +126,21 @@ class AdminCommandController extends Controller $targetUsername = $request->input('username'); $roomId = $request->input('room_id'); $reason = $request->input('reason', '违反聊天室规则'); + $operatorDisplay = $this->buildOperatorDisplayHtml($admin); - // 权限检查(等级由 level_kick 配置) - if (! $this->canExecute($admin, $targetUsername, 'level_kick', '10')) { - return response()->json(['status' => 'error', 'message' => '权限不足'], 403); + // 权限检查:必须拥有职务权限,且不能处理职务高于自己的用户。 + $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_KICK, '踢出'); + if (! $authorization['ok']) { + return response()->json(['status' => 'error', 'message' => $authorization['message']], 403); } // 在强制踢出前补发目标私聊提示,尽量让对方先看到 toast。 $this->pushTargetToastMessage( roomId: (int) $roomId, targetUsername: $targetUsername, - content: "🚫 管理员 {$admin->username} 已将你踢出聊天室。原因:{$reason}", + content: "🚫 {$operatorDisplay} 已将你踢出聊天室。原因:{$reason}", title: '🚫 已被踢出', - toastMessage: "{$admin->username} 已将你踢出聊天室。
原因:{$reason}", + toastMessage: "{$operatorDisplay} 已将你踢出聊天室。
原因:{$reason}", color: '#ef4444', icon: '🚫', ); @@ -150,7 +154,7 @@ class AdminCommandController extends Controller 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', - 'content' => "🚫 管理员 {$admin->username} 已将 {$targetUsername} 踢出聊天室。原因:{$reason}", + 'content' => "🚫 {$operatorDisplay} 已将 {$targetUsername} 踢出聊天室。原因:{$reason}", 'is_secret' => false, 'font_color' => '#dc2626', 'action' => '', @@ -186,10 +190,13 @@ class AdminCommandController extends Controller $targetUsername = $request->input('username'); $roomId = $request->input('room_id'); $duration = $request->input('duration'); + $operatorDisplay = $this->buildOperatorDisplayHtml($admin); + $operatorLabel = $this->buildOperatorIdentityLabel($admin).' '.$admin->username; - // 权限检查(等级由 level_mute 配置) - if (! $this->canExecute($admin, $targetUsername, 'level_mute', '8')) { - return response()->json(['status' => 'error', 'message' => '权限不足'], 403); + // 权限检查:必须拥有职务权限,且不能处理职务高于自己的用户。 + $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_MUTE, '禁言'); + if (! $authorization['ok']) { + return response()->json(['status' => 'error', 'message' => $authorization['message']], 403); } // 设置 Redis 禁言标记,TTL 自动过期 @@ -202,7 +209,7 @@ class AdminCommandController extends Controller 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', - 'content' => "🔇 管理员 {$admin->username} 已将 {$targetUsername} 禁言 {$duration} 分钟。", + 'content' => "🔇 {$operatorDisplay} 已将 {$targetUsername} 禁言 {$duration} 分钟。", 'is_secret' => false, 'font_color' => '#dc2626', 'action' => '', @@ -216,9 +223,9 @@ class AdminCommandController extends Controller $this->pushTargetToastMessage( roomId: (int) $roomId, targetUsername: $targetUsername, - content: "🔇 管理员 {$admin->username} 已将你禁言 {$duration} 分钟。", + content: "🔇 {$operatorDisplay} 已将你禁言 {$duration} 分钟。", title: '🔇 已被禁言', - toastMessage: "{$admin->username} 已将你禁言 {$duration} 分钟。", + toastMessage: "{$operatorDisplay} 已将你禁言 {$duration} 分钟。", color: '#6366f1', icon: '🔇', ); @@ -228,7 +235,8 @@ class AdminCommandController extends Controller roomId: $roomId, username: $targetUsername, muteTime: $duration, - operator: $admin->username, + message: "{$operatorLabel} 已将 [{$targetUsername}] 禁言 {$duration} 分钟。", + operator: $operatorLabel, )); return response()->json(['status' => 'success', 'message' => "已禁言 {$targetUsername} {$duration} 分钟"]); @@ -254,17 +262,16 @@ class AdminCommandController extends Controller $targetUsername = $request->input('username'); $roomId = $request->input('room_id'); $reason = $request->input('reason', '违反聊天室规则'); + $operatorDisplay = $this->buildOperatorDisplayHtml($admin); - // 权限检查(等级由 level_freeze 配置) - if (! $this->canExecute($admin, $targetUsername, 'level_freeze', '14')) { - return response()->json(['status' => 'error', 'message' => '权限不足'], 403); + // 权限检查:必须拥有职务权限,且不能处理职务高于自己的用户。 + $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_FREEZE, '冻结'); + if (! $authorization['ok']) { + return response()->json(['status' => 'error', 'message' => $authorization['message']], 403); } // 冻结用户账号(将等级设为 -1 表示冻结) - $target = User::where('username', $targetUsername)->first(); - if (! $target) { - return response()->json(['status' => 'error', 'message' => '用户不存在'], 404); - } + $target = $authorization['target']; $target->user_level = -1; $target->save(); @@ -272,9 +279,9 @@ class AdminCommandController extends Controller $this->pushTargetToastMessage( roomId: (int) $roomId, targetUsername: $targetUsername, - content: "🧊 管理员 {$admin->username} 已冻结你的账号。原因:{$reason}", + content: "🧊 {$operatorDisplay} 已冻结你的账号。原因:{$reason}", title: '🧊 账号已冻结', - toastMessage: "{$admin->username} 已冻结你的账号。
原因:{$reason}", + toastMessage: "{$operatorDisplay} 已冻结你的账号。
原因:{$reason}", color: '#3b82f6', icon: '🧊', ); @@ -291,7 +298,7 @@ class AdminCommandController extends Controller 'room_id' => $roomId, 'from_user' => '系统传音', 'to_user' => '大家', - 'content' => "🧊 管理员 {$admin->username} 已冻结 {$targetUsername} 的账号。原因:{$reason}", + 'content' => "🧊 {$operatorDisplay} 已冻结 {$targetUsername} 的账号。原因:{$reason}", 'is_secret' => false, 'font_color' => '#dc2626', 'action' => '', @@ -403,13 +410,23 @@ class AdminCommandController extends Controller * 普通在职用户按“部门+职务”显示;站长无在职职务时保持“站长”标识兜底。 */ private function buildAnnouncementPublisherLabel(User $user): string + { + return $this->buildOperatorIdentityLabel($user); + } + + /** + * 生成操作者的身份标签。 + * + * 有在职职务时统一显示为“部门·职务”,无在职职务的 id=1 兜底显示“站长”。 + */ + private function buildOperatorIdentityLabel(User $user): string { $position = $user->activePosition?->position; if ($position) { $departmentName = (string) ($position->department?->name ?? ''); - return $departmentName.$position->name; + return $departmentName ? "{$departmentName}·{$position->name}" : $position->name; } if ($user->id === 1) { @@ -419,6 +436,102 @@ class AdminCommandController extends Controller return '管理员'; } + /** + * 生成操作者在聊天室文案中的完整展示文本。 + */ + private function buildOperatorDisplayHtml(User $user): string + { + $identityLabel = e($this->buildOperatorIdentityLabel($user)); + $username = e($user->username); + + return "{$identityLabel} {$username}"; + } + + /** + * 校验聊天室用户管理动作是否可执行。 + * + * 规则: + * - id=1 站长始终放行 + * - 其他人必须拥有对应职务权限 + * - 不能操作自己 + * - 不能处理 user_level 高于自己的用户 + * - 不能处理部门位阶或职务位阶高于自己的用户 + * + * @return array{ok: bool, message: string, target?: User} + */ + private function authorizeModerationAction( + User $admin, + string $targetUsername, + string $permissionCode, + string $actionLabel, + ): array { + if (! $this->positionPermissionService->hasPermission($admin, $permissionCode)) { + return ['ok' => false, 'message' => "当前职务无权{$actionLabel}用户"]; + } + + if ($admin->username === $targetUsername) { + return ['ok' => false, 'message' => '不能对自己执行该操作']; + } + + $target = User::query() + ->where('username', $targetUsername) + ->with('activePosition.position.department') + ->first(); + + if (! $target) { + return ['ok' => false, 'message' => '用户不存在']; + } + + if (! $this->canModerateTargetByDutyRank($admin, $target)) { + return ['ok' => false, 'message' => '不能处理职务高于自己的用户']; + } + + if ($admin->id !== 1 && $target->user_level > $admin->user_level) { + return ['ok' => false, 'message' => '不能处理等级高于自己的用户']; + } + + return ['ok' => true, 'message' => '校验通过', 'target' => $target]; + } + + /** + * 判断操作者是否可以按职务位阶处理目标用户。 + * + * 规则: + * - 先比较部门 rank + * - 部门相同再比较职务 rank + * - 对方没有在职职务时视为可处理 + * - 同职务平级允许操作,仅禁止处理更高职务 + */ + private function canModerateTargetByDutyRank(User $admin, User $target): bool + { + if ($admin->id === 1) { + return true; + } + + $adminPosition = $admin->activePosition?->load('position.department')->position; + if (! $adminPosition) { + return false; + } + + $targetPosition = $target->activePosition?->position; + if (! $targetPosition) { + return true; + } + + $adminDepartmentRank = (int) ($adminPosition->department?->rank ?? 0); + $targetDepartmentRank = (int) ($targetPosition->department?->rank ?? 0); + + if ($adminDepartmentRank > $targetDepartmentRank) { + return true; + } + + if ($adminDepartmentRank < $targetDepartmentRank) { + return false; + } + + return (int) $adminPosition->rank >= (int) $targetPosition->rank; + } + /** * 管理员全员清屏 * @@ -779,39 +892,6 @@ class AdminCommandController extends Controller ]); } - /** - * 权限检查:管理员是否可对目标用户执行指定操作 - * - * 根据 sysparam 中配置的等级门槛判断权限。 - * - * @param User $admin 管理员用户 - * @param string $targetUsername 目标用户名 - * @param string $levelKey sysparam 中的等级键名(如 level_kick、level_warn) - * @param string $defaultLevel 默认等级值 - * @return bool 是否有权限 - */ - private function canExecute(User $admin, string $targetUsername, string $levelKey, string $defaultLevel = '5'): bool - { - // 必须达到该操作所需的最低等级 - $requiredLevel = (int) Sysparam::getValue($levelKey, $defaultLevel); - if ($admin->user_level < $requiredLevel) { - return false; - } - - // 不能操作自己 - if ($admin->username === $targetUsername) { - return false; - } - - // 目标用户等级不能高于操作者(允许平级互相操作) - $target = User::where('username', $targetUsername)->first(); - if ($target && $target->user_level > $admin->user_level) { - return false; - } - - return true; - } - /** * 向目标用户补发一条系统私聊消息,并附带右下角 toast 配置。 */ diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 365aabd..e2dfc92 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -69,6 +69,8 @@ class UserController extends Controller 'position_name' => $activePosition?->name ?? '', 'position_icon' => $activePosition?->icon ?? '', 'department_name' => $activePosition?->department?->name ?? '', + 'department_rank' => (int) ($activePosition?->department?->rank ?? 0), + 'position_rank' => (int) ($activePosition?->rank ?? 0), ]; // 只有等级不低于对方,或者自己看自己时,才能看到详细的财富、经验资产 diff --git a/app/Support/PositionPermissionRegistry.php b/app/Support/PositionPermissionRegistry.php index f054167..2337025 100644 --- a/app/Support/PositionPermissionRegistry.php +++ b/app/Support/PositionPermissionRegistry.php @@ -43,6 +43,26 @@ class PositionPermissionRegistry */ public const ROOM_FULLSCREEN_EFFECT = 'room.fullscreen_effect'; + /** + * 用户警告权限。 + */ + public const USER_WARN = 'room.user_warn'; + + /** + * 用户踢出权限。 + */ + public const USER_KICK = 'room.user_kick'; + + /** + * 用户禁言权限。 + */ + public const USER_MUTE = 'room.user_mute'; + + /** + * 用户冻结权限。 + */ + public const USER_FREEZE = 'room.user_freeze'; + /** * 返回全部权限定义。 * @@ -81,6 +101,26 @@ class PositionPermissionRegistry 'label' => '全屏特效', 'description' => '允许触发聊天室内全部全屏动画特效。', ], + self::USER_WARN => [ + 'group' => '用户管理', + 'label' => '警告用户', + 'description' => '允许在用户名片内对低于自身职务的用户发送警告。', + ], + self::USER_KICK => [ + 'group' => '用户管理', + 'label' => '踢出用户', + 'description' => '允许在用户名片内将低于自身职务的用户踢出当前聊天室。', + ], + self::USER_MUTE => [ + 'group' => '用户管理', + 'label' => '禁言用户', + 'description' => '允许在用户名片内对低于自身职务的用户执行禁言。', + ], + self::USER_FREEZE => [ + 'group' => '用户管理', + 'label' => '冻结用户', + 'description' => '允许在用户名片内冻结低于自身职务的用户账号。', + ], ]; } diff --git a/resources/views/chat/frame.blade.php b/resources/views/chat/frame.blade.php index f8b7af2..d5bbdb8 100644 --- a/resources/views/chat/frame.blade.php +++ b/resources/views/chat/frame.blade.php @@ -27,6 +27,9 @@ $superLevel = (int) \App\Models\Sysparam::getValue('superlevel', '100'); $myLevel = Auth::user()->user_level; $positionPermissions = array_keys(array_filter($roomPermissionMap ?? [])); + $operatorActivePosition = Auth::user()->activePosition?->load('position.department')->position; + $operatorDepartmentRank = (int) ($operatorActivePosition?->department?->rank ?? 0); + $operatorPositionRank = (int) ($operatorActivePosition?->rank ?? 0); @endphp