后台用户编辑页接入职务任命流程
This commit is contained in:
@@ -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' => '所选职务不存在,请重新选择。',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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' => [],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user