将用户管理操作接入职务权限体系

This commit is contained in:
2026-04-21 18:00:02 +08:00
parent a066580014
commit b0028c515f
8 changed files with 462 additions and 85 deletions
+143 -63
View File
@@ -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' => "⚠️ 管理员 <b>{$admin->username}</b> 警告 <b>{$targetUsername}</b>{$reason}",
'content' => "⚠️ {$operatorDisplay} 警告 <b>{$targetUsername}</b>{$reason}",
'is_secret' => false,
'font_color' => '#dc2626',
'action' => '',
@@ -94,9 +96,9 @@ class AdminCommandController extends Controller
$this->pushTargetToastMessage(
roomId: (int) $roomId,
targetUsername: $targetUsername,
content: "⚠️ 管理员 <b>{$admin->username}</b> 警告了你:{$reason}",
content: "⚠️ {$operatorDisplay} 警告了你:{$reason}",
title: '⚠️ 收到警告',
toastMessage: "<b>{$admin->username}</b> 警告了你:{$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: "🚫 管理员 <b>{$admin->username}</b> 已将你踢出聊天室。原因:{$reason}",
content: "🚫 {$operatorDisplay} 已将你踢出聊天室。原因:{$reason}",
title: '🚫 已被踢出',
toastMessage: "<b>{$admin->username}</b> 已将你踢出聊天室。<br>原因:{$reason}",
toastMessage: "{$operatorDisplay} 已将你踢出聊天室。<br>原因:{$reason}",
color: '#ef4444',
icon: '🚫',
);
@@ -150,7 +154,7 @@ class AdminCommandController extends Controller
'room_id' => $roomId,
'from_user' => '系统传音',
'to_user' => '大家',
'content' => "🚫 管理员 <b>{$admin->username}</b> 已将 <b>{$targetUsername}</b> 踢出聊天室。原因:{$reason}",
'content' => "🚫 {$operatorDisplay} 已将 <b>{$targetUsername}</b> 踢出聊天室。原因:{$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' => "🔇 管理员 <b>{$admin->username}</b> 已将 <b>{$targetUsername}</b> 禁言 {$duration} 分钟。",
'content' => "🔇 {$operatorDisplay} 已将 <b>{$targetUsername}</b> 禁言 {$duration} 分钟。",
'is_secret' => false,
'font_color' => '#dc2626',
'action' => '',
@@ -216,9 +223,9 @@ class AdminCommandController extends Controller
$this->pushTargetToastMessage(
roomId: (int) $roomId,
targetUsername: $targetUsername,
content: "🔇 管理员 <b>{$admin->username}</b> 已将你禁言 {$duration} 分钟。",
content: "🔇 {$operatorDisplay} 已将你禁言 {$duration} 分钟。",
title: '🔇 已被禁言',
toastMessage: "<b>{$admin->username}</b> 已将你禁言 <b>{$duration}</b> 分钟。",
toastMessage: "{$operatorDisplay} 已将你禁言 <b>{$duration}</b> 分钟。",
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: "🧊 管理员 <b>{$admin->username}</b> 已冻结你的账号。原因:{$reason}",
content: "🧊 {$operatorDisplay} 已冻结你的账号。原因:{$reason}",
title: '🧊 账号已冻结',
toastMessage: "<b>{$admin->username}</b> 已冻结你的账号。<br>原因:{$reason}",
toastMessage: "{$operatorDisplay} 已冻结你的账号。<br>原因:{$reason}",
color: '#3b82f6',
icon: '🧊',
);
@@ -291,7 +298,7 @@ class AdminCommandController extends Controller
'room_id' => $roomId,
'from_user' => '系统传音',
'to_user' => '大家',
'content' => "🧊 管理员 <b>{$admin->username}</b> 已冻结 <b>{$targetUsername}</b> 的账号。原因:{$reason}",
'content' => "🧊 {$operatorDisplay} 已冻结 <b>{$targetUsername}</b> 的账号。原因:{$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 "<b>{$identityLabel}</b> <b>{$username}</b>";
}
/**
* 校验聊天室用户管理动作是否可执行。
*
* 规则:
* - 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 配置。
*/
+2
View File
@@ -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),
];
// 只有等级不低于对方,或者自己看自己时,才能看到详细的财富、经验资产