后台用户编辑页接入职务任命流程

This commit is contained in:
2026-04-21 17:26:52 +08:00
parent a17a67f533
commit 5a6446b832
5 changed files with 349 additions and 16 deletions
@@ -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: '你的职务已被撤销,页面权限正在同步更新。',
));
}
}
@@ -0,0 +1,57 @@
<?php
/**
* 文件功能:后台用户资料更新请求校验
*/
namespace App\Http\Requests\Admin;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Foundation\Http\FormRequest;
/**
* 类功能:集中校验后台用户编辑弹窗提交的资料字段。
*/
class UpdateManagedUserRequest extends FormRequest
{
/**
* 方法功能:允许已通过路由中间件的后台用户继续执行校验。
*/
public function authorize(): bool
{
return true;
}
/**
* 方法功能:返回后台用户编辑表单的校验规则。
*
* @return array<string, ValidationRule|array<mixed>|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<string, string>
*/
public function messages(): array
{
return [
'position_id.exists' => '所选职务不存在,请重新选择。',
];
}
}
+3
View File
@@ -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;
@@ -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">
</div>
{{-- 任命职务 --}}
<div class="mt-4">
<label class="block text-xs font-bold text-gray-600 mb-1">职务</label>
<select name="position_id" x-model="editingUser.position_id"
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 p-2 border text-sm">
<option value="">无职务</option>
@foreach ($departments as $department)
<optgroup label="{{ $department->name }}">
@foreach ($department->positions as $position)
<option value="{{ $position->id }}">{{ $position->icon }} {{ $position->name }}</option>
@endforeach
</optgroup>
@endforeach
</select>
</div>
{{-- VIP 会员设置 --}}
<div class="mt-4 grid grid-cols-2 gap-4">
<div>
@@ -0,0 +1,133 @@
<?php
/**
* 文件功能:后台用户管理编辑功能测试
*
* 覆盖后台用户编辑页对任命职务体系的复用,
* 确保用户资料弹窗可以直接任命、切换或撤销用户职务。
*/
namespace Tests\Feature\Feature;
use App\Models\Department;
use App\Models\Position;
use App\Models\User;
use App\Models\UserPosition;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* 类功能:验证后台用户管理页面的用户资料编辑行为。
*/
class AdminUserManagerTest extends TestCase
{
use RefreshDatabase;
/**
* 方法功能:验证站长可以在用户编辑页直接任命职务。
*/
public function test_site_owner_can_assign_position_from_user_editor(): void
{
$siteOwner = $this->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' => [],
]);
}
}