将用户管理操作接入职务权限体系
This commit is contained in:
@@ -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 配置。
|
||||
*/
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
|
||||
// 只有等级不低于对方,或者自己看自己时,才能看到详细的财富、经验资产
|
||||
|
||||
@@ -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' => '允许在用户名片内冻结低于自身职务的用户账号。',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
<script>
|
||||
window.chatContext = {
|
||||
@@ -84,6 +87,8 @@
|
||||
$posName = $activePos?->position?->name ?? '';
|
||||
@endphp
|
||||
welcomePrefix: "{{ $deptName ? "{$deptName} {$posName} {$user->username}" : $user->username }}",
|
||||
operatorDepartmentRank: {{ $operatorDepartmentRank }},
|
||||
operatorPositionRank: {{ $operatorPositionRank }},
|
||||
myMaxReward: @php
|
||||
if (Auth::id() === 1) {
|
||||
// 超级管理员(id=1)无需职务,直接拥有不限量奖励权
|
||||
|
||||
@@ -131,6 +131,48 @@
|
||||
$confirm: (...args) => window.chatDialog.confirm(...args),
|
||||
$prompt: (...args) => window.chatDialog.prompt(...args),
|
||||
|
||||
/**
|
||||
* 判断当前操作者是否拥有指定的职务权限码。
|
||||
*/
|
||||
hasPositionPermission(permissionCode) {
|
||||
return Boolean(window.chatContext?.positionPermissionMap?.[permissionCode]);
|
||||
},
|
||||
|
||||
/**
|
||||
* 判断目标用户职务是否严格低于当前操作者。
|
||||
*
|
||||
* 规则:
|
||||
* 1. 先比较部门位阶 rank
|
||||
* 2. 部门相同再比较职务位阶 rank
|
||||
* 3. 对方没有在职职务时,视为可处理
|
||||
* 4. id=1 站长始终可处理
|
||||
*/
|
||||
canManageTargetByDuty() {
|
||||
if (window.chatContext?.isSiteOwner) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const targetDepartmentRank = Number(this.userInfo.department_rank || 0);
|
||||
const targetPositionRank = Number(this.userInfo.position_rank || 0);
|
||||
|
||||
if (targetDepartmentRank <= 0 && targetPositionRank <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const operatorDepartmentRank = Number(window.chatContext?.operatorDepartmentRank || 0);
|
||||
const operatorPositionRank = Number(window.chatContext?.operatorPositionRank || 0);
|
||||
|
||||
if (operatorDepartmentRank > targetDepartmentRank) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operatorDepartmentRank < targetDepartmentRank) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return operatorPositionRank >= targetPositionRank;
|
||||
},
|
||||
|
||||
/** 切换好友关系(加好友 / 删好友) */
|
||||
async toggleFriend() {
|
||||
if (this.friendLoading) return;
|
||||
@@ -984,11 +1026,15 @@
|
||||
</div>
|
||||
|
||||
{{-- 管理操作 + 职务操作 合并折叠区 --}}
|
||||
@if (
|
||||
$myLevel >= $levelWarn ||
|
||||
$room->master == Auth::user()->username ||
|
||||
Auth::user()->activePosition ||
|
||||
$myLevel >= $superLevel)
|
||||
@php
|
||||
$canWarnUser = Auth::id() === 1 || (($roomPermissionMap[\App\Support\PositionPermissionRegistry::USER_WARN] ?? false) === true);
|
||||
$canKickUser = Auth::id() === 1 || (($roomPermissionMap[\App\Support\PositionPermissionRegistry::USER_KICK] ?? false) === true);
|
||||
$canMuteUser = Auth::id() === 1 || (($roomPermissionMap[\App\Support\PositionPermissionRegistry::USER_MUTE] ?? false) === true);
|
||||
$canFreezeUser = Auth::id() === 1 || (($roomPermissionMap[\App\Support\PositionPermissionRegistry::USER_FREEZE] ?? false) === true);
|
||||
$hasUserModerationPermission = $canWarnUser || $canKickUser || $canMuteUser || $canFreezeUser;
|
||||
$hasPositionActions = Auth::user()->activePosition || $myLevel >= $superLevel;
|
||||
@endphp
|
||||
@if ($hasUserModerationPermission || $hasPositionActions)
|
||||
<div style="padding: 0 16px 12px;" x-show="userInfo.username !== window.chatContext.username">
|
||||
|
||||
{{-- 折叠标题 --}}
|
||||
@@ -1004,31 +1050,31 @@
|
||||
{{-- 折叠内容 --}}
|
||||
<div x-show="showAdminPanel" x-transition style="display: none; margin-top: 6px;">
|
||||
|
||||
@if ($myLevel >= $levelWarn || $room->master == Auth::user()->username)
|
||||
<div x-show="userInfo.user_level <= {{ $myLevel }}">
|
||||
@if ($hasUserModerationPermission)
|
||||
<div x-show="canManageTargetByDuty()">
|
||||
<div style="font-size: 10px; color: #9ca3af; margin-bottom: 4px; padding-left: 2px;">
|
||||
管理员操作
|
||||
</div>
|
||||
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 8px;">
|
||||
@if ($myLevel >= $levelWarn)
|
||||
@if ($canWarnUser)
|
||||
<button
|
||||
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #fef3c7; border: 1px solid #f59e0b; cursor: pointer;"
|
||||
x-on:click="warnUser()">⚠️ 警告
|
||||
</button>
|
||||
@endif
|
||||
@if ($myLevel >= $levelKick)
|
||||
@if ($canKickUser)
|
||||
<button
|
||||
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #fee2e2; border: 1px solid #ef4444; cursor: pointer;"
|
||||
x-on:click="kickUser()">🚫 踢出
|
||||
</button>
|
||||
@endif
|
||||
@if ($myLevel >= $levelMute)
|
||||
@if ($canMuteUser)
|
||||
<button
|
||||
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #e0e7ff; border: 1px solid #6366f1; cursor: pointer;"
|
||||
x-on:click="isMuting = !isMuting">🔇 禁言
|
||||
</button>
|
||||
@endif
|
||||
@if ($myLevel >= $levelFreeze)
|
||||
@if ($canFreezeUser)
|
||||
<button
|
||||
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #dbeafe; border: 1px solid #3b82f6; cursor: pointer;"
|
||||
x-on:click="freezeUser()">🧊 冻结
|
||||
@@ -1036,7 +1082,7 @@
|
||||
@endif
|
||||
|
||||
{{-- 职务奖励金币(凭空产生),仅有在职职务且 max_reward != 0 的人可见 --}}
|
||||
@if (Auth::user()->activePosition || $myLevel >= $superLevel)
|
||||
@if ($hasPositionActions)
|
||||
<button x-show="window.chatContext?.myMaxReward !== 0"
|
||||
style="flex:1; padding: 5px; border-radius: 4px; font-size: 11px; background: #fef3c7; border: 1px solid #f59e0b; cursor: pointer;"
|
||||
x-on:click="openRewardModal(userInfo.username)">💰 奖励金币
|
||||
@@ -1046,7 +1092,7 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (Auth::user()->activePosition || $myLevel >= $superLevel)
|
||||
@if ($hasPositionActions)
|
||||
<div>
|
||||
<div style="font-size: 10px; color: #9ca3af; margin-bottom: 4px; padding-left: 2px;">
|
||||
职务操作
|
||||
|
||||
@@ -215,6 +215,43 @@ class ChatControllerTest extends TestCase
|
||||
$response->assertDontSee("runAdminAction('announce-message')", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用户名片中的用户管理按钮会按职务权限显示。
|
||||
*/
|
||||
public function test_room_view_renders_only_granted_user_management_buttons(): void
|
||||
{
|
||||
$room = Room::create(['room_name' => 'usermenu']);
|
||||
$user = $this->createUserWithPositionPermissions([
|
||||
PositionPermissionRegistry::USER_WARN,
|
||||
PositionPermissionRegistry::USER_MUTE,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('chat.room', $room->id));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('⚠️ 警告', false);
|
||||
$response->assertSee('🔇 禁言', false);
|
||||
$response->assertDontSee('🚫 踢出', false);
|
||||
$response->assertDontSee('🧊 冻结', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试没有用户管理权限时,用户名片中不会渲染相关按钮。
|
||||
*/
|
||||
public function test_room_view_hides_user_management_buttons_without_permissions(): void
|
||||
{
|
||||
$room = Room::create(['room_name' => 'nousermenu']);
|
||||
$user = $this->createUserWithPositionPermissions([]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('chat.room', $room->id));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertDontSee('⚠️ 警告', false);
|
||||
$response->assertDontSee('🚫 踢出', false);
|
||||
$response->assertDontSee('🔇 禁言', false);
|
||||
$response->assertDontSee('🧊 冻结', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试站长即使没有在职职务,也能看到管理菜单中的刷新全员按钮。
|
||||
*/
|
||||
|
||||
@@ -139,7 +139,7 @@ class AdminCommandControllerTest extends TestCase
|
||||
|
||||
$this->assertNotNull($publicMessage);
|
||||
$this->assertStringContainsString(
|
||||
$admin->activePosition->position->department->name.$admin->activePosition->position->name,
|
||||
$admin->activePosition->position->department->name.'·'.$admin->activePosition->position->name,
|
||||
(string) $publicMessage['content']
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
@@ -297,7 +297,7 @@ class AdminCommandControllerTest extends TestCase
|
||||
'message' => "已警告 {$target->username}",
|
||||
]);
|
||||
|
||||
$privateMessage = $this->findPrivateSystemMessage($room->id, $target->username, '管理员 <b>'.$admin->username.'</b> 警告了你');
|
||||
$privateMessage = $this->findPrivateSystemMessage($room->id, $target->username, '站长</b> <b>'.$admin->username.'</b> 警告了你');
|
||||
|
||||
$this->assertNotNull($privateMessage);
|
||||
$this->assertSame('⚠️ 收到警告', $privateMessage['toast_notification']['title'] ?? null);
|
||||
@@ -336,6 +336,40 @@ class AdminCommandControllerTest extends TestCase
|
||||
Queue::assertPushed(SaveMessageJob::class, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试拥有警告权限的职务用户发送的文案会带上部门和职务。
|
||||
*/
|
||||
public function test_position_user_with_warn_permission_uses_department_and_position_in_message(): void
|
||||
{
|
||||
Queue::fake();
|
||||
|
||||
$admin = $this->createPositionedManager([
|
||||
PositionPermissionRegistry::USER_WARN,
|
||||
], departmentRank: 95, positionRank: 95);
|
||||
$admin->load('activePosition.position.department');
|
||||
$target = User::factory()->create([
|
||||
'user_level' => 1,
|
||||
]);
|
||||
$room = Room::create([
|
||||
'room_name' => '职务警告房',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.warn'), [
|
||||
'username' => $target->username,
|
||||
'room_id' => $room->id,
|
||||
'reason' => '请遵守秩序',
|
||||
]);
|
||||
|
||||
$response->assertOk()->assertJson([
|
||||
'status' => 'success',
|
||||
]);
|
||||
|
||||
$identityText = $admin->activePosition->position->department->name.'·'.$admin->activePosition->position->name;
|
||||
$privateMessage = $this->findPrivateSystemMessage($room->id, $target->username, "{$identityText}</b> <b>{$admin->username}</b> 警告了你");
|
||||
|
||||
$this->assertNotNull($privateMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试踢出操作会给目标用户写入带 toast 的私聊提示。
|
||||
*/
|
||||
@@ -395,6 +429,91 @@ class AdminCommandControllerTest extends TestCase
|
||||
Queue::assertPushed(SaveMessageJob::class, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试缺少踢出权限的职务用户会被拒绝。
|
||||
*/
|
||||
public function test_position_user_without_kick_permission_cannot_kick_target(): void
|
||||
{
|
||||
$admin = $this->createPositionedManager([
|
||||
PositionPermissionRegistry::USER_WARN,
|
||||
]);
|
||||
$target = User::factory()->create([
|
||||
'user_level' => 1,
|
||||
]);
|
||||
$room = Room::create([
|
||||
'room_name' => '无权踢人房',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.kick'), [
|
||||
'username' => $target->username,
|
||||
'room_id' => $room->id,
|
||||
'reason' => '测试',
|
||||
]);
|
||||
|
||||
$response->assertStatus(403)->assertJson([
|
||||
'status' => 'error',
|
||||
'message' => '当前职务无权踢出用户',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试不能处理部门位阶更高的目标用户。
|
||||
*/
|
||||
public function test_position_user_cannot_warn_target_from_higher_rank_department(): void
|
||||
{
|
||||
$admin = $this->createPositionedManager([
|
||||
PositionPermissionRegistry::USER_WARN,
|
||||
], departmentRank: 80, positionRank: 80);
|
||||
$target = $this->createTargetUserWithPositionRanks(departmentRank: 90, positionRank: 70);
|
||||
$room = Room::create([
|
||||
'room_name' => '高部门位阶房',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.warn'), [
|
||||
'username' => $target->username,
|
||||
'room_id' => $room->id,
|
||||
'reason' => '测试',
|
||||
]);
|
||||
|
||||
$response->assertStatus(403)->assertJson([
|
||||
'status' => 'error',
|
||||
'message' => '不能处理职务高于自己的用户',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试同部门下不能处理职务位阶更高的目标用户。
|
||||
*/
|
||||
public function test_position_user_cannot_mute_target_with_higher_rank_in_same_department(): void
|
||||
{
|
||||
$department = Department::create([
|
||||
'name' => '同部门位阶测试',
|
||||
'rank' => 88,
|
||||
'color' => '#0f766e',
|
||||
'sort_order' => 1,
|
||||
'description' => '同部门位阶限制测试',
|
||||
]);
|
||||
|
||||
$admin = $this->createPositionedManager([
|
||||
PositionPermissionRegistry::USER_MUTE,
|
||||
], departmentRank: 88, positionRank: 60, existingDepartment: $department);
|
||||
$target = $this->createTargetUserWithPositionRanks(departmentRank: 88, positionRank: 70, existingDepartment: $department);
|
||||
$room = Room::create([
|
||||
'room_name' => '同部门高位阶房',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($admin)->postJson(route('command.mute'), [
|
||||
'username' => $target->username,
|
||||
'room_id' => $room->id,
|
||||
'duration' => 10,
|
||||
]);
|
||||
|
||||
$response->assertStatus(403)->assertJson([
|
||||
'status' => 'error',
|
||||
'message' => '不能处理职务高于自己的用户',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建管理员命令测试共用的操作者、目标用户和房间。
|
||||
*
|
||||
@@ -421,15 +540,19 @@ class AdminCommandControllerTest extends TestCase
|
||||
*
|
||||
* @param list<string> $permissions
|
||||
*/
|
||||
private function createPositionedManager(array $permissions): User
|
||||
{
|
||||
private function createPositionedManager(
|
||||
array $permissions,
|
||||
int $departmentRank = 90,
|
||||
int $positionRank = 90,
|
||||
?Department $existingDepartment = null,
|
||||
): User {
|
||||
$user = User::factory()->create([
|
||||
'user_level' => 90,
|
||||
'user_level' => $positionRank,
|
||||
]);
|
||||
|
||||
$department = Department::create([
|
||||
$department = $existingDepartment ?? Department::create([
|
||||
'name' => '命令权限部'.$user->id,
|
||||
'rank' => 90,
|
||||
'rank' => $departmentRank,
|
||||
'color' => '#7c3aed',
|
||||
'sort_order' => 1,
|
||||
'description' => '聊天室命令权限测试',
|
||||
@@ -439,8 +562,8 @@ class AdminCommandControllerTest extends TestCase
|
||||
'department_id' => $department->id,
|
||||
'name' => '命令权限职务'.$user->id,
|
||||
'icon' => '🛠️',
|
||||
'rank' => 90,
|
||||
'level' => 90,
|
||||
'rank' => $positionRank,
|
||||
'level' => $positionRank,
|
||||
'sort_order' => 1,
|
||||
'permissions' => $permissions,
|
||||
]);
|
||||
@@ -457,6 +580,48 @@ class AdminCommandControllerTest extends TestCase
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带指定部门/职务位阶的目标用户。
|
||||
*/
|
||||
private function createTargetUserWithPositionRanks(
|
||||
int $departmentRank,
|
||||
int $positionRank,
|
||||
?Department $existingDepartment = null,
|
||||
): User {
|
||||
$user = User::factory()->create([
|
||||
'user_level' => $positionRank,
|
||||
]);
|
||||
|
||||
$department = $existingDepartment ?? Department::create([
|
||||
'name' => '目标用户部门'.$user->id,
|
||||
'rank' => $departmentRank,
|
||||
'color' => '#1d4ed8',
|
||||
'sort_order' => 1,
|
||||
'description' => '目标用户位阶测试',
|
||||
]);
|
||||
|
||||
$position = Position::create([
|
||||
'department_id' => $department->id,
|
||||
'name' => '目标用户职务'.$user->id,
|
||||
'icon' => '🎖️',
|
||||
'rank' => $positionRank,
|
||||
'level' => $positionRank,
|
||||
'sort_order' => 1,
|
||||
'permissions' => [],
|
||||
]);
|
||||
|
||||
UserPosition::create([
|
||||
'user_id' => $user->id,
|
||||
'position_id' => $position->id,
|
||||
'appointed_by_user_id' => null,
|
||||
'appointed_at' => now(),
|
||||
'remark' => '目标用户位阶测试',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
return $user->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从房间消息缓存中定位目标用户收到的系统私聊提示。
|
||||
*
|
||||
|
||||
@@ -157,6 +157,8 @@ class AdminPositionPermissionTest extends TestCase
|
||||
$response->assertSee('权限管理');
|
||||
$response->assertSee('设置公告');
|
||||
$response->assertSee('礼包红包');
|
||||
$response->assertSee('警告用户');
|
||||
$response->assertSee('冻结用户');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user