From 5a6446b832eb1818f678f50cb689a378eb297811 Mon Sep 17 00:00:00 2001 From: lkddi Date: Tue, 21 Apr 2026 17:26:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=94=A8=E6=88=B7=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E9=A1=B5=E6=8E=A5=E5=85=A5=E8=81=8C=E5=8A=A1=E4=BB=BB?= =?UTF-8?q?=E5=91=BD=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/UserManagerController.php | 155 ++++++++++++++++-- .../Admin/UpdateManagedUserRequest.php | 57 +++++++ app/Models/User.php | 3 + resources/views/admin/users/index.blade.php | 17 ++ .../Feature/Feature/AdminUserManagerTest.php | 133 +++++++++++++++ 5 files changed, 349 insertions(+), 16 deletions(-) create mode 100644 app/Http/Requests/Admin/UpdateManagedUserRequest.php create mode 100644 tests/Feature/Feature/AdminUserManagerTest.php diff --git a/app/Http/Controllers/Admin/UserManagerController.php b/app/Http/Controllers/Admin/UserManagerController.php index 3294879..6580a01 100644 --- a/app/Http/Controllers/Admin/UserManagerController.php +++ b/app/Http/Controllers/Admin/UserManagerController.php @@ -12,8 +12,15 @@ namespace App\Http\Controllers\Admin; use App\Enums\CurrencySource; +use App\Events\AppointmentAnnounced; +use App\Events\UserBrowserRefreshRequested; use App\Http\Controllers\Controller; +use App\Http\Requests\Admin\UpdateManagedUserRequest; +use App\Models\Department; +use App\Models\Position; use App\Models\User; +use App\Models\UserPosition; +use App\Services\AppointmentService; use App\Services\ChatStateService; use App\Services\UserCurrencyService; use Illuminate\Http\JsonResponse; @@ -23,6 +30,9 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\View\View; +/** + * 类功能:负责后台用户列表展示、资料编辑与删除操作。 + */ class UserManagerController extends Controller { /** @@ -31,6 +41,7 @@ class UserManagerController extends Controller public function __construct( private readonly UserCurrencyService $currencyService, private readonly ChatStateService $chatState, + private readonly AppointmentService $appointmentService, ) {} /** @@ -77,8 +88,12 @@ class UserManagerController extends Controller // 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', 'sortBy', 'sortDir', 'onlineUsernames')); + return view('admin.users.index', compact('users', 'vipLevels', 'departments', 'sortBy', 'sortDir', 'onlineUsernames')); } /** @@ -86,10 +101,11 @@ class UserManagerController extends Controller * * @param User $user 路由模型自动注入 */ - public function update(Request $request, User $user): JsonResponse|RedirectResponse + public function update(UpdateManagedUserRequest $request, User $user): JsonResponse|RedirectResponse { $targetUser = $user; $currentUser = Auth::user(); + $responseMessages = []; // 超级管理员专属:仅 id=1 的账号可编辑用户信息 if ($currentUser->id !== 1) { @@ -104,17 +120,7 @@ class UserManagerController extends Controller return response()->json(['status' => 'error', 'message' => '权限不足:您无法修改同级或高级管理人员资料。'], 403); } - $validated = $request->validate([ - 'sex' => 'sometimes|integer|in:0,1,2', - 'exp_num' => 'sometimes|integer|min:0', - 'jjb' => 'sometimes|integer|min:0', - 'meili' => 'sometimes|integer|min:0', - 'qianming' => 'sometimes|nullable|string|max:255', - 'headface' => 'sometimes|string|max:50', - 'password' => 'nullable|string|min:6', - 'vip_level_id' => 'sometimes|nullable|integer|exists:vip_levels,id', - 'hy_time' => 'sometimes|nullable|date', - ]); + $validated = $request->validated(); if (isset($validated['sex'])) { $targetUser->sex = $validated['sex']; @@ -188,11 +194,31 @@ class UserManagerController extends Controller $targetUser->save(); - if ($request->wantsJson()) { - return response()->json(['status' => 'success', 'message' => '用户资料已强行更新完毕!']); + 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']; + } } - return back()->with('success', '用户资料已更新!'); + 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)); } /** @@ -225,4 +251,101 @@ class UserManagerController extends Controller 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: '你的职务已被撤销,页面权限正在同步更新。', + )); + } } diff --git a/app/Http/Requests/Admin/UpdateManagedUserRequest.php b/app/Http/Requests/Admin/UpdateManagedUserRequest.php new file mode 100644 index 0000000..4f4eadc --- /dev/null +++ b/app/Http/Requests/Admin/UpdateManagedUserRequest.php @@ -0,0 +1,57 @@ +|string> + */ + public function rules(): array + { + return [ + 'sex' => ['sometimes', 'integer', 'in:0,1,2'], + 'exp_num' => ['sometimes', 'integer', 'min:0'], + 'jjb' => ['sometimes', 'integer', 'min:0'], + 'meili' => ['sometimes', 'integer', 'min:0'], + 'qianming' => ['sometimes', 'nullable', 'string', 'max:255'], + 'position_id' => ['sometimes', 'nullable', 'integer', 'exists:positions,id'], + 'headface' => ['sometimes', 'string', 'max:50'], + 'password' => ['nullable', 'string', 'min:6'], + 'vip_level_id' => ['sometimes', 'nullable', 'integer', 'exists:vip_levels,id'], + 'hy_time' => ['sometimes', 'nullable', 'date'], + ]; + } + + /** + * 方法功能:返回后台用户编辑表单的中文错误提示。 + * + * @return array + */ + public function messages(): array + { + return [ + 'position_id.exists' => '所选职务不存在,请重新选择。', + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 32aebf4..89d24ba 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -21,6 +21,9 @@ use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +/** + * 类功能:封装聊天室用户资料、会员状态与职务关联等核心数据行为。 + */ class User extends Authenticatable { use HasFactory, Notifiable; diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php index ccffde5..fdda01a 100644 --- a/resources/views/admin/users/index.blade.php +++ b/resources/views/admin/users/index.blade.php @@ -156,6 +156,7 @@ meili: {{ $user->meili ?? 0 }}, sex: '{{ $user->sex }}', qianming: '{{ addslashes($user->qianming ?? '') }}', + position_id: '{{ $user->activePosition?->position_id ?? '' }}', visit_num: {{ $user->visit_num ?? 0 }}, vip_level_id: '{{ $user->vip_level_id ?? '' }}', hy_time: '{{ $user->hy_time ? $user->hy_time->format('Y-m-d') : '' }}', @@ -260,6 +261,22 @@ class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm"> + {{-- 任命职务 --}} +
+ + +
+ {{-- VIP 会员设置 --}}
diff --git a/tests/Feature/Feature/AdminUserManagerTest.php b/tests/Feature/Feature/AdminUserManagerTest.php new file mode 100644 index 0000000..5cb6c4d --- /dev/null +++ b/tests/Feature/Feature/AdminUserManagerTest.php @@ -0,0 +1,133 @@ +createSiteOwner(); + $targetUser = User::factory()->create(); + $position = $this->createPosition('招商主管', '🎖️', 66); + + $response = $this->actingAs($siteOwner) + ->putJson(route('admin.users.update', $targetUser), [ + 'exp_num' => $targetUser->exp_num ?? 0, + 'jjb' => $targetUser->jjb ?? 0, + 'meili' => $targetUser->meili ?? 0, + 'sex' => $targetUser->sex ?? 0, + 'qianming' => $targetUser->qianming, + 'position_id' => $position->id, + ]); + + $response->assertOk() + ->assertJson([ + 'status' => 'success', + ]); + + $activePosition = $targetUser->fresh()->activePosition; + + $this->assertNotNull($activePosition); + $this->assertSame($position->id, $activePosition?->position_id); + $this->assertSame($position->level, $targetUser->fresh()->user_level); + } + + /** + * 方法功能:验证站长可以在用户编辑页撤销现有职务。 + */ + public function test_site_owner_can_revoke_position_from_user_editor(): void + { + $siteOwner = $this->createSiteOwner(); + $targetUser = User::factory()->create([ + 'exp_num' => 0, + 'user_level' => 66, + ]); + $position = $this->createPosition('值班主持', '🎤', 66); + + UserPosition::create([ + 'user_id' => $targetUser->id, + 'position_id' => $position->id, + 'appointed_by_user_id' => $siteOwner->id, + 'appointed_at' => now(), + 'is_active' => true, + ]); + + $response = $this->actingAs($siteOwner) + ->putJson(route('admin.users.update', $targetUser), [ + 'exp_num' => $targetUser->exp_num ?? 0, + 'jjb' => $targetUser->jjb ?? 0, + 'meili' => $targetUser->meili ?? 0, + 'sex' => $targetUser->sex ?? 0, + 'qianming' => $targetUser->qianming, + 'position_id' => null, + ]); + + $response->assertOk() + ->assertJson([ + 'status' => 'success', + ]); + + $targetUser->refresh(); + + $this->assertNull($targetUser->activePosition); + $this->assertSame(1, $targetUser->user_level); + } + + /** + * 方法功能:创建测试用站长账号。 + */ + private function createSiteOwner(): User + { + return User::factory()->create([ + 'id' => 1, + 'user_level' => 100, + ]); + } + + /** + * 方法功能:创建测试用部门与职务。 + */ + private function createPosition(string $name, string $icon, int $level): Position + { + $department = Department::create([ + 'name' => '测试部门-'.$name, + 'rank' => 90, + 'color' => '#334155', + 'sort_order' => 1, + 'description' => '后台用户管理测试部门', + ]); + + return Position::create([ + 'department_id' => $department->id, + 'name' => $name, + 'icon' => $icon, + 'rank' => $level, + 'level' => $level, + 'max_persons' => 5, + 'sort_order' => 1, + 'permissions' => [], + ]); + } +}