diff --git a/app/Http/Controllers/Admin/SystemController.php b/app/Http/Controllers/Admin/SystemController.php
index b891ffd..95673c4 100644
--- a/app/Http/Controllers/Admin/SystemController.php
+++ b/app/Http/Controllers/Admin/SystemController.php
@@ -118,6 +118,15 @@ class SystemController extends Controller
*/
private function isDedicatedAlias(string $alias): bool
{
- return in_array($alias, ['levelexp'], true);
+ return in_array($alias, [
+ 'levelexp',
+ 'level_warn',
+ 'level_mute',
+ 'level_kick',
+ 'level_announcement',
+ 'level_ban',
+ 'level_banip',
+ 'level_freeze',
+ ], true);
}
}
diff --git a/app/Http/Controllers/Admin/UserManagerController.php b/app/Http/Controllers/Admin/UserManagerController.php
index 6580a01..f914360 100644
--- a/app/Http/Controllers/Admin/UserManagerController.php
+++ b/app/Http/Controllers/Admin/UserManagerController.php
@@ -241,10 +241,10 @@ class UserManagerController extends Controller
abort(403, '权限不足:无法删除同级或高级账号!');
}
- // 管理员保护:达到踢人等级(level_kick)的用户视为管理员,不可被强杀
- $levelKick = (int) \App\Models\Sysparam::getValue('level_kick', '10');
- if ($targetUser->user_level >= $levelKick) {
- abort(403, '该用户为管理员,不允许强杀!请先在用户编辑中降低其等级。');
+ // 任命体系保护:仍持有在职职务的账号不可直接强杀,必须先走撤职流程。
+ $targetUser->loadMissing('activePosition.position');
+ if ($targetUser->id === 1 || $targetUser->activePosition?->position) {
+ abort(403, '该用户当前拥有在职职务,不允许强杀!请先撤销职务。');
}
$targetUser->delete();
diff --git a/app/Http/Controllers/AdminCommandController.php b/app/Http/Controllers/AdminCommandController.php
index 5272f00..62a6639 100644
--- a/app/Http/Controllers/AdminCommandController.php
+++ b/app/Http/Controllers/AdminCommandController.php
@@ -4,7 +4,7 @@
* 文件功能:管理员聊天室实时命令控制器
*
* 提供管理员在聊天室内对用户执行的管理操作:
- * 警告(=J)、踢出(=T)、禁言(=B)、冻结(=Y)、查看私信(=S)、职务公屏讲话。
+ * 警告(=J)、踢出(=T)、禁言(=B)、封号、封IP、查看私信(=S)、职务公屏讲话。
*
* 对应原 ASP 文件:DOUSER.ASP / KILLUSER.ASP / LOCKIP.ASP / NEWSAY.ASP
*
@@ -278,14 +278,14 @@ class AdminCommandController extends Controller
}
/**
- * 冻结用户账号(=Y 理由)
+ * 封禁用户账号。
*
- * 将用户账号状态设为冻结,禁止登录。
+ * 将目标账号设为封禁状态,并将其从当前在线房间中移出。
*
- * @param Request $request 请求对象,需包含 username, reason
+ * @param Request $request 请求对象,需包含 username, room_id, reason
* @return JsonResponse 操作结果
*/
- public function freeze(Request $request): JsonResponse
+ public function ban(Request $request): JsonResponse
{
$request->validate([
'username' => 'required|string',
@@ -301,12 +301,12 @@ class AdminCommandController extends Controller
return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
}
- $reason = ChatContentSanitizer::htmlText($request->input('reason', '违反聊天室规则'));
+ $reason = ChatContentSanitizer::htmlText($request->input('reason', '严重违规'));
$safeTargetUsername = ChatContentSanitizer::htmlText($targetUsername);
$operatorDisplay = $this->buildOperatorDisplayHtml($admin);
// 权限检查:必须拥有职务权限,且不能处理职务高于自己的用户。
- $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_FREEZE, '冻结');
+ $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_BAN, '封禁');
if (! $authorization['ok']) {
return response()->json(['status' => 'error', 'message' => $authorization['message']], 403);
}
@@ -316,37 +316,30 @@ class AdminCommandController extends Controller
return response()->json(['status' => 'error', 'message' => $targetAuthorization['message']], 403);
}
- // 冻结用户账号(将等级设为 -1 表示冻结)
$target = $authorization['target'];
$target->user_level = -1;
$target->save();
- // 先给被冻结用户补发私聊提示,再将其移出各房间并强制下线。
$this->pushTargetToastMessage(
- roomId: (int) $roomId,
+ roomId: $roomId,
targetUsername: $targetUsername,
- content: "🧊 {$operatorDisplay} 已冻结你的账号。原因:{$reason}",
- title: '🧊 账号已冻结',
- toastMessage: "{$operatorDisplay} 已冻结你的账号。
原因:{$reason}",
- color: '#3b82f6',
- icon: '🧊',
+ content: "⛔ {$operatorDisplay} 已封禁你的账号。原因:{$reason}",
+ title: '⛔ 账号已封禁',
+ toastMessage: "{$operatorDisplay} 已封禁你的账号。
原因:{$reason}",
+ color: '#991b1b',
+ icon: '⛔',
);
- // 从所有房间移除
- $rooms = $this->chatState->getUserRooms($targetUsername);
- foreach ($rooms as $rid) {
- $this->chatState->userLeave($rid, $targetUsername);
- }
+ $this->removeUserFromAllRooms($targetUsername);
- // 广播冻结消息
$msg = [
'id' => $this->chatState->nextMessageId($roomId),
'room_id' => $roomId,
'from_user' => '系统传音',
'to_user' => '大家',
- 'content' => "🧊 {$operatorDisplay} 已冻结 {$safeTargetUsername} 的账号。原因:{$reason}",
+ 'content' => "⛔ {$operatorDisplay} 已封禁 {$safeTargetUsername} 的账号。原因:{$reason}",
'is_secret' => false,
- 'font_color' => '#dc2626',
+ 'font_color' => '#991b1b',
'action' => '',
'sent_at' => now()->toDateTimeString(),
];
@@ -354,10 +347,93 @@ class AdminCommandController extends Controller
broadcast(new MessageSent($roomId, $msg));
SaveMessageJob::dispatch($msg);
- // 广播踢出事件
- broadcast(new \App\Events\UserKicked($roomId, $targetUsername, "账号已被冻结:{$reason}"));
+ broadcast(new \App\Events\UserKicked($roomId, $targetUsername, "账号已被封禁:{$reason}"));
- return response()->json(['status' => 'success', 'message' => "已冻结 {$targetUsername} 的账号"]);
+ return response()->json(['status' => 'success', 'message' => "已封禁 {$targetUsername} 的账号"]);
+ }
+
+ /**
+ * 封禁用户 IP。
+ *
+ * 将目标账号设为封禁状态并把最近登录 IP 写入黑名单。
+ *
+ * @param Request $request 请求对象,需包含 username, room_id, reason
+ * @return JsonResponse 操作结果
+ */
+ public function banIp(Request $request): JsonResponse
+ {
+ $request->validate([
+ 'username' => 'required|string',
+ 'room_id' => 'required|integer',
+ 'reason' => 'nullable|string|max:200',
+ ]);
+
+ $admin = Auth::user();
+ $targetUsername = $request->input('username');
+ $roomId = (int) $request->input('room_id');
+ $roomAuthorization = $this->authorizeManagedRoom($roomId, $admin);
+ if (! $roomAuthorization['ok']) {
+ return response()->json(['status' => 'error', 'message' => $roomAuthorization['message']], 403);
+ }
+
+ $reason = ChatContentSanitizer::htmlText($request->input('reason', '严重违规'));
+ $safeTargetUsername = ChatContentSanitizer::htmlText($targetUsername);
+ $operatorDisplay = $this->buildOperatorDisplayHtml($admin);
+
+ // 权限检查:必须拥有职务权限,且不能处理职务高于自己的用户。
+ $authorization = $this->authorizeModerationAction($admin, $targetUsername, PositionPermissionRegistry::USER_BANIP, '封禁 IP ');
+ if (! $authorization['ok']) {
+ return response()->json(['status' => 'error', 'message' => $authorization['message']], 403);
+ }
+
+ $targetAuthorization = $this->authorizeTargetOnlineInRoom($roomId, $targetUsername);
+ if (! $targetAuthorization['ok']) {
+ return response()->json(['status' => 'error', 'message' => $targetAuthorization['message']], 403);
+ }
+
+ $target = $authorization['target'];
+ $targetIp = (string) ($target->last_ip ?? '');
+ if ($targetIp !== '') {
+ Redis::sadd('banned_ips', $targetIp);
+ }
+
+ $target->user_level = -1;
+ $target->save();
+
+ $this->pushTargetToastMessage(
+ roomId: $roomId,
+ targetUsername: $targetUsername,
+ content: "🌐 {$operatorDisplay} 已封禁你的 IP 并冻结账号。原因:{$reason}",
+ title: '🌐 IP 已封禁',
+ toastMessage: "{$operatorDisplay} 已封禁你的 IP 并冻结账号。
原因:{$reason}",
+ color: '#7c2d12',
+ icon: '🌐',
+ );
+
+ $this->removeUserFromAllRooms($targetUsername);
+
+ $ipSuffix = $targetIp !== '' ? "(IP:{$targetIp})" : '(未记录有效 IP)';
+ $msg = [
+ 'id' => $this->chatState->nextMessageId($roomId),
+ 'room_id' => $roomId,
+ 'from_user' => '系统传音',
+ 'to_user' => '大家',
+ 'content' => "🌐 {$operatorDisplay} 已封禁 {$safeTargetUsername} 的 IP 并冻结账号。原因:{$reason} {$ipSuffix}",
+ 'is_secret' => false,
+ 'font_color' => '#9a3412',
+ 'action' => '',
+ 'sent_at' => now()->toDateTimeString(),
+ ];
+ $this->chatState->pushMessage($roomId, $msg);
+ broadcast(new MessageSent($roomId, $msg));
+ SaveMessageJob::dispatch($msg);
+
+ broadcast(new \App\Events\UserKicked($roomId, $targetUsername, "IP 已被封禁:{$reason}"));
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => $targetIp !== '' ? "已封禁 {$targetUsername} 的 IP 与账号" : "已封禁 {$targetUsername} 的账号,但未记录可封禁 IP",
+ ]);
}
/**
@@ -555,7 +631,7 @@ class AdminCommandController extends Controller
string $permissionCode,
string $actionLabel,
): array {
- if (! $this->positionPermissionService->hasPermission($admin, $permissionCode)) {
+ if ($admin->id !== 1 && ! $this->positionPermissionService->hasPermission($admin, $permissionCode)) {
return ['ok' => false, 'message' => "当前职务无权{$actionLabel}用户"];
}
@@ -583,6 +659,18 @@ class AdminCommandController extends Controller
return ['ok' => true, 'message' => '校验通过', 'target' => $target];
}
+ /**
+ * 将目标用户从当前在线的全部房间中移除。
+ */
+ private function removeUserFromAllRooms(string $targetUsername): void
+ {
+ $rooms = $this->chatState->getUserRooms($targetUsername);
+
+ foreach ($rooms as $rid) {
+ $this->chatState->userLeave((int) $rid, $targetUsername);
+ }
+ }
+
/**
* 判断操作者是否可以按职务位阶处理目标用户。
*
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index 3190caa..d3ea516 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -2,13 +2,8 @@
/**
* 文件功能:用户中心与管理控制器
- * 接管原版 USERinfo.ASP, USERSET.ASP, chpasswd.asp, KILLUSER.ASP, LOCKIP.ASP
- *
- * 权限等级通过 sysparam 表动态配置:
- * level_kick - 踢人所需等级
- * level_mute - 禁言所需等级
- * level_ban - 封号所需等级
- * level_banip - 封IP所需等级
+ * 接管原版 USERinfo.ASP, USERSET.ASP 与 chpasswd.asp。
+ * 聊天室管理动作已统一迁移到 AdminCommandController 的职务权限链路。
*
* @author ChatRoom Laravel
*
@@ -18,25 +13,23 @@
namespace App\Http\Controllers;
use App\Enums\CurrencySource;
-use App\Events\UserKicked;
-use App\Events\UserMuted;
use App\Events\UserStatusUpdated;
use App\Http\Requests\ChangePasswordRequest;
use App\Http\Requests\RevealProfileInfoRequest;
use App\Http\Requests\UpdateChatPreferencesRequest;
use App\Http\Requests\UpdateDailyStatusRequest;
use App\Http\Requests\UpdateProfileRequest;
-use App\Models\Room;
use App\Models\Sysparam;
use App\Models\User;
use App\Services\ChatStateService;
use App\Services\ChatUserPresenceService;
+use App\Services\PositionPermissionService;
use App\Services\UserCurrencyService;
+use App\Support\PositionPermissionRegistry;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
-use Illuminate\Support\Facades\Redis;
/**
* 类功能:处理用户资料、聊天室偏好、当日状态与基础管理动作。
@@ -65,6 +58,7 @@ class UserController extends Controller
private readonly ChatStateService $chatState,
private readonly ChatUserPresenceService $chatUserPresenceService,
private readonly UserCurrencyService $currencyService,
+ private readonly PositionPermissionService $positionPermissionService,
) {}
/**
@@ -166,9 +160,10 @@ class UserController extends Controller
] : null,
];
- // 拥有封禁IP(level_banip)或踢人以上权限的管理,可以查看IP和归属地
- $levelBanIp = (int) Sysparam::getValue('level_banip', '15');
- if ($operator && $operator->user_level >= $levelBanIp) {
+ // 管理员网络信息仅对站长或拥有「封IP」职务权限的操作者展示。
+ $canViewNetworkInfo = $operator
+ && ($operator->id === 1 || $this->positionPermissionService->hasPermission($operator, PositionPermissionRegistry::USER_BANIP));
+ if ($canViewNetworkInfo) {
$data['first_ip'] = $targetUser->first_ip;
// last_ip 目前定义为『上次登录IP』(取数据库 previous_ip)
$data['last_ip'] = $targetUser->previous_ip;
@@ -415,7 +410,7 @@ class UserController extends Controller
/**
* 生成微信绑定代码
*/
- public function generateWechatCode(\Illuminate\Http\Request $request): JsonResponse
+ public function generateWechatCode(Request $request): JsonResponse
{
$user = \Illuminate\Support\Facades\Auth::user();
if (! $user) {
@@ -435,7 +430,7 @@ class UserController extends Controller
/**
* 取消绑定微信
*/
- public function unbindWechat(\Illuminate\Http\Request $request): JsonResponse
+ public function unbindWechat(Request $request): JsonResponse
{
$user = \Illuminate\Support\Facades\Auth::user();
if (! $user) {
@@ -451,162 +446,6 @@ class UserController extends Controller
]);
}
- /**
- * 通用权限校验:检查操作者是否有权操作目标用户
- *
- * @param object $operator 操作者
- * @param string $targetUsername 目标用户名
- * @param int $roomId 房间ID
- * @param string $levelKey sysparam中的等级键名(如 level_kick)
- * @param string $actionName 操作名称(用于错误提示)
- * @return array{room: Room, target: User}|JsonResponse
- */
- private function checkPermission(object $operator, string $targetUsername, int $roomId, string $levelKey, string $actionName): array|JsonResponse
- {
- $room = Room::findOrFail($roomId);
- $requiredLevel = (int) Sysparam::getValue($levelKey, '15');
-
- // 鉴权:操作者要是房间房主或达到所需等级
- if ($room->master !== $operator->username && $operator->user_level < $requiredLevel) {
- return response()->json(['status' => 'error', 'message' => "权限不足(需要{$requiredLevel}级),无法执行{$actionName}操作。"], 403);
- }
-
- $targetUser = User::where('username', $targetUsername)->first();
- if (! $targetUser) {
- return response()->json(['status' => 'error', 'message' => '目标用户不存在。'], 404);
- }
-
- // 防误伤:不能操作等级 >= 自己的人
- if ($targetUser->user_level >= $operator->user_level) {
- return response()->json(['status' => 'error', 'message' => "权限不足,无法对同级或高级用户执行{$actionName}。"], 403);
- }
-
- return ['room' => $room, 'target' => $targetUser];
- }
-
- /**
- * 踢出房间 (对应 KILLUSER.ASP)
- * 所需等级由 sysparam level_kick 配置
- */
- public function kick(Request $request, string $username): JsonResponse
- {
- $operator = Auth::user();
- $roomId = $request->input('room_id');
-
- if (! $roomId) {
- return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
- }
-
- $result = $this->checkPermission($operator, $username, $roomId, 'level_kick', '踢出');
- if ($result instanceof JsonResponse) {
- return $result;
- }
-
- // 广播踢出事件
- broadcast(new UserKicked($roomId, $result['target']->username, "管理员 [{$operator->username}] 将 [{$result['target']->username}] 踢出了聊天室。"));
-
- return response()->json(['status' => 'success', 'message' => "已成功将 {$result['target']->username} 踢出房间。"]);
- }
-
- /**
- * 禁言 (对应原版限制功能)
- * 所需等级由 sysparam level_mute 配置
- * 禁言信息存入 Redis,TTL 到期自动解除
- */
- public function mute(Request $request, string $username): JsonResponse
- {
- $operator = Auth::user();
- $roomId = $request->input('room_id');
- $duration = (int) $request->input('duration', 5);
-
- if (! $roomId) {
- return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
- }
-
- $result = $this->checkPermission($operator, $username, $roomId, 'level_mute', '禁言');
- if ($result instanceof JsonResponse) {
- return $result;
- }
-
- // 写入 Redis 禁言标记,TTL = 禁言分钟数 * 60
- Redis::setex("mute:{$roomId}:{$username}", $duration * 60, json_encode([
- 'operator' => $operator->username,
- 'reason' => '管理员禁言',
- 'until' => now()->addMinutes($duration)->toDateTimeString(),
- ]));
-
- // 广播禁言事件
- broadcast(new UserMuted($roomId, $username, $duration));
-
- return response()->json(['status' => 'success', 'message' => "已对 {$username} 实施禁言 {$duration} 分钟。"]);
- }
-
- /**
- * 封号(禁止登录)
- * 所需等级由 sysparam level_ban 配置
- * 将用户等级设为 -1 表示封禁
- */
- public function ban(Request $request, string $username): JsonResponse
- {
- $operator = Auth::user();
- $roomId = $request->input('room_id');
-
- if (! $roomId) {
- return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
- }
-
- $result = $this->checkPermission($operator, $username, $roomId, 'level_ban', '封号');
- if ($result instanceof JsonResponse) {
- return $result;
- }
-
- // 封号:设置等级为 -1
- $result['target']->user_level = -1;
- $result['target']->save();
-
- // 踢出聊天室
- broadcast(new UserKicked($roomId, $username, "管理员 [{$operator->username}] 已封禁用户 [{$username}] 的账号。"));
-
- return response()->json(['status' => 'success', 'message' => "用户 {$username} 已被封号。"]);
- }
-
- /**
- * 封IP(记录IP到黑名单并踢出)
- * 所需等级由 sysparam level_banip 配置
- */
- public function banIp(Request $request, string $username): JsonResponse
- {
- $operator = Auth::user();
- $roomId = $request->input('room_id');
-
- if (! $roomId) {
- return response()->json(['status' => 'error', 'message' => '缺少房间参数。'], 422);
- }
-
- $result = $this->checkPermission($operator, $username, $roomId, 'level_banip', '封IP');
- if ($result instanceof JsonResponse) {
- return $result;
- }
-
- $targetIp = $result['target']->last_ip;
-
- if ($targetIp) {
- // 将IP加入 Redis 黑名单(永久)
- Redis::sadd('banned_ips', $targetIp);
- }
-
- // 同时封号
- $result['target']->user_level = -1;
- $result['target']->save();
-
- // 踢出聊天室
- broadcast(new UserKicked($roomId, $username, "管理员 [{$operator->username}] 已封禁用户 [{$username}] 的IP地址。"));
-
- $ipInfo = $targetIp ? "(IP: {$targetIp})" : '(未记录IP)';
-
- return response()->json(['status' => 'success', 'message' => "用户 {$username} 已被封号并封IP{$ipInfo}。"]);
- }
-
/**
* 判断操作者是否可以免费查看目标用户经验、金币与魅力。
*/
diff --git a/app/Models/PositionAuthorityLog.php b/app/Models/PositionAuthorityLog.php
index b6cb7de..963e9a5 100644
--- a/app/Models/PositionAuthorityLog.php
+++ b/app/Models/PositionAuthorityLog.php
@@ -58,6 +58,7 @@ class PositionAuthorityLog extends Model
'warn' => '警告',
'kick' => '踢出',
'mute' => '禁言',
+ 'ban' => '封号',
'banip' => '封锁IP',
'other' => '其他',
];
diff --git a/app/Support/PositionPermissionRegistry.php b/app/Support/PositionPermissionRegistry.php
index 1ec7c78..3ffc56a 100644
--- a/app/Support/PositionPermissionRegistry.php
+++ b/app/Support/PositionPermissionRegistry.php
@@ -64,9 +64,14 @@ class PositionPermissionRegistry
public const USER_MUTE = 'room.user_mute';
/**
- * 用户冻结权限。
+ * 用户封号权限。
*/
- public const USER_FREEZE = 'room.user_freeze';
+ public const USER_BAN = 'room.user_ban';
+
+ /**
+ * 用户封IP权限。
+ */
+ public const USER_BANIP = 'room.user_banip';
/**
* 返回全部权限定义。
@@ -126,10 +131,15 @@ class PositionPermissionRegistry
'label' => '禁言用户',
'description' => '允许在用户名片内对低于自身职务的用户执行禁言。',
],
- self::USER_FREEZE => [
+ self::USER_BAN => [
'group' => '用户管理',
- 'label' => '冻结用户',
- 'description' => '允许在用户名片内冻结低于自身职务的用户账号。',
+ 'label' => '封号用户',
+ 'description' => '允许在用户名片内封禁低于自身职务的用户账号并强制下线。',
+ ],
+ self::USER_BANIP => [
+ 'group' => '用户管理',
+ 'label' => '封IP',
+ 'description' => '允许在用户名片内封禁低于自身职务的用户 IP,并查看管理员网络信息。',
],
];
}
diff --git a/database/migrations/2026_02_26_092923_create_sysparam_table.php b/database/migrations/2026_02_26_092923_create_sysparam_table.php
index b85fc62..d559993 100644
--- a/database/migrations/2026_02_26_092923_create_sysparam_table.php
+++ b/database/migrations/2026_02_26_092923_create_sysparam_table.php
@@ -45,13 +45,6 @@ return new class extends Migration
['alias' => 'jjb_per_heartbeat', 'body' => '1-3', 'guidetxt' => '💰 每次心跳金币奖励(支持固定值如"1"或范围如"1-5";0关闭)', 'created_at' => $now, 'updated_at' => $now],
// ── 管理操作权限等级 ──────────────────────────────────────
- ['alias' => 'level_warn', 'body' => '5', 'guidetxt' => '警告所需等级', 'created_at' => $now, 'updated_at' => $now],
- ['alias' => 'level_mute', 'body' => '50', 'guidetxt' => '禁言所需等级', 'created_at' => $now, 'updated_at' => $now],
- ['alias' => 'level_kick', 'body' => '60', 'guidetxt' => '踢人所需等级', 'created_at' => $now, 'updated_at' => $now],
- ['alias' => 'level_announcement', 'body' => '60', 'guidetxt' => '设置公告所需等级', 'created_at' => $now, 'updated_at' => $now],
- ['alias' => 'level_ban', 'body' => '80', 'guidetxt' => '封号所需等级', 'created_at' => $now, 'updated_at' => $now],
- ['alias' => 'level_banip', 'body' => '90', 'guidetxt' => '封IP所需等级', 'created_at' => $now, 'updated_at' => $now],
- ['alias' => 'level_freeze', 'body' => '14', 'guidetxt' => '冻结账号所需等级', 'created_at' => $now, 'updated_at' => $now],
// ── 随机事件 ──────────────────────────────────────────────
['alias' => 'auto_event_chance', 'body' => '10', 'guidetxt' => '随机事件触发概率(百分比,1-100)', 'created_at' => $now, 'updated_at' => $now],
diff --git a/database/migrations/2026_04_26_203922_remove_legacy_level_permission_sysparams.php b/database/migrations/2026_04_26_203922_remove_legacy_level_permission_sysparams.php
new file mode 100644
index 0000000..dd2ecf4
--- /dev/null
+++ b/database/migrations/2026_04_26_203922_remove_legacy_level_permission_sysparams.php
@@ -0,0 +1,58 @@
+whereIn('alias', [
+ 'level_warn',
+ 'level_mute',
+ 'level_kick',
+ 'level_announcement',
+ 'level_ban',
+ 'level_banip',
+ 'level_freeze',
+ ])
+ ->delete();
+ }
+
+ /**
+ * 方法功能:回滚时恢复旧等级权限参数默认值。
+ */
+ public function down(): void
+ {
+ $now = now();
+
+ foreach ([
+ 'level_warn' => ['5', '警告所需等级'],
+ 'level_mute' => ['50', '禁言所需等级'],
+ 'level_kick' => ['60', '踢人所需等级'],
+ 'level_announcement' => ['60', '设置公告所需等级'],
+ 'level_ban' => ['80', '封号所需等级'],
+ 'level_banip' => ['90', '封IP所需等级'],
+ 'level_freeze' => ['14', '冻结账号所需等级'],
+ ] as $alias => [$body, $guidetxt]) {
+ DB::table('sysparam')->updateOrInsert(
+ ['alias' => $alias],
+ [
+ 'body' => $body,
+ 'guidetxt' => $guidetxt,
+ 'created_at' => $now,
+ 'updated_at' => $now,
+ ]
+ );
+ }
+ }
+};
diff --git a/database/migrations/2026_04_26_205012_merge_freeze_permission_into_ban_permission.php b/database/migrations/2026_04_26_205012_merge_freeze_permission_into_ban_permission.php
new file mode 100644
index 0000000..b7f8fe9
--- /dev/null
+++ b/database/migrations/2026_04_26_205012_merge_freeze_permission_into_ban_permission.php
@@ -0,0 +1,58 @@
+each(function (Position $position): void {
+ $permissions = array_values($position->permissions ?? []);
+
+ if (! in_array('room.user_freeze', $permissions, true)) {
+ return;
+ }
+
+ $normalizedPermissions = array_values(array_unique(array_map(
+ fn (string $permission): string => $permission === 'room.user_freeze' ? 'room.user_ban' : $permission,
+ $permissions,
+ )));
+
+ $position->forceFill([
+ 'permissions' => $normalizedPermissions,
+ ])->save();
+ });
+ }
+
+ /**
+ * 方法功能:回滚时把合并后的封号权限恢复为冻结权限。
+ */
+ public function down(): void
+ {
+ Position::query()->each(function (Position $position): void {
+ $permissions = array_values($position->permissions ?? []);
+
+ if (! in_array('room.user_ban', $permissions, true)) {
+ return;
+ }
+
+ $normalizedPermissions = array_values(array_unique(array_map(
+ fn (string $permission): string => $permission === 'room.user_ban' ? 'room.user_freeze' : $permission,
+ $permissions,
+ )));
+
+ $position->forceFill([
+ 'permissions' => $normalizedPermissions,
+ ])->save();
+ });
+ }
+};
diff --git a/resources/js/chat-room/user-card.js b/resources/js/chat-room/user-card.js
index 17469cf..1b94e77 100644
--- a/resources/js/chat-room/user-card.js
+++ b/resources/js/chat-room/user-card.js
@@ -603,18 +603,49 @@ export function userCardComponent() {
}
},
- /** 冻结用户 */
- async freezeUser() {
+ /** 封禁账号 */
+ async banUser() {
const confirmed = await this.$confirm(
- '确定要冻结 ' + this.userInfo.username + ' 的账号吗?冻结后将无法登录!',
- '冻结账号',
- '#cc4444'
+ '确定要封禁 ' + this.userInfo.username + ' 的账号吗?封禁后将无法登录。',
+ '封禁账号',
+ '#991b1b'
);
if (!confirmed) return;
- const reason = await this.$prompt('冻结原因:', '严重违规', '填写原因', '#cc4444');
+ const reason = await this.$prompt('封禁原因:', '严重违规', '填写原因', '#991b1b');
if (reason === null) return;
try {
- const res = await fetch('/command/freeze', {
+ const res = await fetch('/command/ban', {
+ method: 'POST',
+ headers: this._headers(),
+ body: JSON.stringify({
+ username: this.userInfo.username,
+ room_id: window.chatContext.roomId,
+ reason: reason || '严重违规'
+ })
+ });
+ const data = await res.json();
+ if (data.status === 'success') {
+ this.showUserModal = false;
+ } else {
+ this.$alert('操作失败:' + data.message, '操作失败', '#cc4444');
+ }
+ } catch (e) {
+ this.$alert('网络异常', '错误', '#cc4444');
+ }
+ },
+
+ /** 封禁 IP */
+ async banIpUser() {
+ const confirmed = await this.$confirm(
+ '确定要封禁 ' + this.userInfo.username + ' 的 IP 吗?该操作会同时冻结账号。',
+ '封禁 IP',
+ '#9a3412'
+ );
+ if (!confirmed) return;
+ const reason = await this.$prompt('封禁原因:', '严重违规', '填写原因', '#9a3412');
+ if (reason === null) return;
+ try {
+ const res = await fetch('/command/banip', {
method: 'POST',
headers: this._headers(),
body: JSON.stringify({
diff --git a/resources/views/admin/positions/index.blade.php b/resources/views/admin/positions/index.blade.php
index a8b7da0..643cfde 100644
--- a/resources/views/admin/positions/index.blade.php
+++ b/resources/views/admin/positions/index.blade.php
@@ -442,6 +442,7 @@
权限管理
(控制聊天室输入框上方「管理」菜单中可见的功能按钮)
+
聊天室管理动作已统一按职务权限控制,不再使用等级阈值参数。
等级越高,可使用的管理功能越多。双击用户名片中可执行以下操作:
+聊天室管理动作已统一按职务权限控制。被任命到具备对应权限的职务后,双击用户名片即可执行以下操作: