2026-02-28 23:44:38 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 文件功能:聊天室内快速任命/撤销控制器
|
|
|
|
|
|
* 供有职务的管理员在聊天室用户名片弹窗中快速任命或撤销目标用户的职务。
|
|
|
|
|
|
* 权限校验委托给 AppointmentService,本控制器只做请求解析和返回 JSON。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @author ChatRoom Laravel
|
|
|
|
|
|
*
|
|
|
|
|
|
* @version 1.0.0
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
|
|
|
|
|
|
use App\Events\AppointmentAnnounced;
|
2026-04-14 22:41:33 +08:00
|
|
|
|
use App\Events\MessageSent;
|
|
|
|
|
|
use App\Jobs\SaveMessageJob;
|
2026-02-28 23:44:38 +08:00
|
|
|
|
use App\Models\Position;
|
|
|
|
|
|
use App\Models\User;
|
|
|
|
|
|
use App\Services\AppointmentService;
|
|
|
|
|
|
use App\Services\ChatStateService;
|
|
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
|
use Illuminate\Support\Facades\Auth;
|
|
|
|
|
|
|
|
|
|
|
|
class ChatAppointmentController extends Controller
|
|
|
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 注入任命服务
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function __construct(
|
|
|
|
|
|
private readonly AppointmentService $appointmentService,
|
|
|
|
|
|
private readonly ChatStateService $chatState,
|
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取可用职务列表(供名片弹窗下拉选择)
|
|
|
|
|
|
* 返回操作人有权限任命的职务
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function positions(): JsonResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
$operator = Auth::user();
|
|
|
|
|
|
$superLevel = (int) \App\Models\Sysparam::getValue('superlevel', '100');
|
|
|
|
|
|
$operatorPosition = $operator->activePosition?->position;
|
|
|
|
|
|
|
|
|
|
|
|
$query = Position::query()
|
|
|
|
|
|
->with('department')
|
|
|
|
|
|
->orderByDesc('rank');
|
|
|
|
|
|
|
|
|
|
|
|
// 仅有具体职务(非 superlevel 直通)的操作人才限制 rank 范围
|
|
|
|
|
|
if ($operatorPosition && $operator->user_level < $superLevel) {
|
|
|
|
|
|
$query->where('rank', '<', $operatorPosition->rank);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$positions = $query->get()->map(fn ($p) => [
|
|
|
|
|
|
'id' => $p->id,
|
|
|
|
|
|
'name' => $p->name,
|
|
|
|
|
|
'icon' => $p->icon,
|
|
|
|
|
|
'rank' => $p->rank,
|
|
|
|
|
|
'department' => $p->department?->name,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
return response()->json(['status' => 'success', 'positions' => $positions]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 快速任命:将目标用户任命为指定职务
|
|
|
|
|
|
* 成功后向操作人所在聊天室广播任命公告
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function appoint(Request $request): JsonResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
$request->validate([
|
|
|
|
|
|
'username' => 'required|string|exists:users,username',
|
|
|
|
|
|
'position_id' => 'required|exists:positions,id',
|
|
|
|
|
|
'remark' => 'nullable|string|max:100',
|
|
|
|
|
|
'room_id' => 'nullable|integer|exists:rooms,id',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$operator = Auth::user();
|
|
|
|
|
|
$target = User::where('username', $request->username)->firstOrFail();
|
|
|
|
|
|
$position = Position::with('department')->findOrFail($request->position_id);
|
|
|
|
|
|
|
|
|
|
|
|
$result = $this->appointmentService->appoint($operator, $target, $position, $request->remark);
|
|
|
|
|
|
|
|
|
|
|
|
// 任命成功后广播礼花通知:优先用前端传来的 room_id,否则从 Redis 查操作人所在房间
|
|
|
|
|
|
if ($result['ok']) {
|
|
|
|
|
|
$roomId = $request->integer('room_id') ?: ($this->chatState->getUserRooms($operator->username)[0] ?? null);
|
|
|
|
|
|
if ($roomId) {
|
|
|
|
|
|
broadcast(new AppointmentAnnounced(
|
|
|
|
|
|
roomId: (int) $roomId,
|
|
|
|
|
|
targetUsername: $target->username,
|
|
|
|
|
|
positionIcon: $position->icon ?? '🎖️',
|
|
|
|
|
|
positionName: $position->name,
|
|
|
|
|
|
departmentName: $position->department?->name ?? '',
|
|
|
|
|
|
operatorName: $operator->username,
|
|
|
|
|
|
));
|
2026-04-14 22:41:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 给被任命用户补一条私聊提示,并复用右下角 toast 通知。
|
|
|
|
|
|
$this->pushTargetToastMessage(
|
|
|
|
|
|
roomId: (int) $roomId,
|
|
|
|
|
|
targetUsername: $target->username,
|
|
|
|
|
|
content: "✨ <b>{$operator->username}</b> 已任命你为 {$position->icon} {$position->name}。",
|
|
|
|
|
|
title: '✨ 职务任命通知',
|
|
|
|
|
|
toastMessage: "<b>{$operator->username}</b> 已任命你为 <b>{$position->icon} {$position->name}</b>。",
|
|
|
|
|
|
color: '#a855f7',
|
|
|
|
|
|
icon: '✨',
|
|
|
|
|
|
);
|
2026-02-28 23:44:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
|
'status' => $result['ok'] ? 'success' : 'error',
|
|
|
|
|
|
'message' => $result['message'],
|
|
|
|
|
|
], $result['ok'] ? 200 : 422);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 快速撤销:撤销目标用户当前的职务
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function revoke(Request $request): JsonResponse
|
|
|
|
|
|
{
|
|
|
|
|
|
$request->validate([
|
|
|
|
|
|
'username' => 'required|string|exists:users,username',
|
|
|
|
|
|
'remark' => 'nullable|string|max:100',
|
|
|
|
|
|
'room_id' => 'nullable|integer|exists:rooms,id',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
$operator = Auth::user();
|
|
|
|
|
|
$target = User::where('username', $request->username)->firstOrFail();
|
|
|
|
|
|
|
|
|
|
|
|
// 撤销前先取目标当前职务信息(撤销后就查不到了)
|
|
|
|
|
|
$activeUp = $target->activePosition?->load('position.department');
|
|
|
|
|
|
$posIcon = $activeUp?->position?->icon ?? '🎖️';
|
|
|
|
|
|
$posName = $activeUp?->position?->name ?? '';
|
|
|
|
|
|
$deptName = $activeUp?->position?->department?->name ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
$result = $this->appointmentService->revoke($operator, $target, $request->remark);
|
|
|
|
|
|
|
|
|
|
|
|
// 撤销成功后广播通知到聊天室
|
|
|
|
|
|
if ($result['ok'] && $posName) {
|
|
|
|
|
|
$roomId = $request->integer('room_id') ?: ($this->chatState->getUserRooms($operator->username)[0] ?? null);
|
|
|
|
|
|
if ($roomId) {
|
|
|
|
|
|
broadcast(new AppointmentAnnounced(
|
|
|
|
|
|
roomId: (int) $roomId,
|
|
|
|
|
|
targetUsername: $target->username,
|
|
|
|
|
|
positionIcon: $posIcon,
|
|
|
|
|
|
positionName: $posName,
|
|
|
|
|
|
departmentName: $deptName,
|
|
|
|
|
|
operatorName: $operator->username,
|
|
|
|
|
|
type: 'revoke',
|
|
|
|
|
|
));
|
2026-04-14 22:41:33 +08:00
|
|
|
|
|
|
|
|
|
|
// 给被撤职用户补一条私聊提示,并复用右下角 toast 通知。
|
|
|
|
|
|
$this->pushTargetToastMessage(
|
|
|
|
|
|
roomId: (int) $roomId,
|
|
|
|
|
|
targetUsername: $target->username,
|
|
|
|
|
|
content: "📋 <b>{$operator->username}</b> 已撤销你的 {$posIcon} {$posName} 职务。",
|
|
|
|
|
|
title: '📋 职务变动通知',
|
|
|
|
|
|
toastMessage: "<b>{$operator->username}</b> 已撤销你的 <b>{$posIcon} {$posName}</b> 职务。",
|
|
|
|
|
|
color: '#6b7280',
|
|
|
|
|
|
icon: '📋',
|
|
|
|
|
|
);
|
2026-02-28 23:44:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
|
'status' => $result['ok'] ? 'success' : 'error',
|
|
|
|
|
|
'message' => $result['message'],
|
|
|
|
|
|
], $result['ok'] ? 200 : 422);
|
|
|
|
|
|
}
|
2026-04-14 22:41:33 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 向目标用户补发一条系统私聊消息,并附带右下角 toast 配置。
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function pushTargetToastMessage(
|
|
|
|
|
|
int $roomId,
|
|
|
|
|
|
string $targetUsername,
|
|
|
|
|
|
string $content,
|
|
|
|
|
|
string $title,
|
|
|
|
|
|
string $toastMessage,
|
|
|
|
|
|
string $color,
|
|
|
|
|
|
string $icon,
|
|
|
|
|
|
): void {
|
|
|
|
|
|
$msg = [
|
|
|
|
|
|
'id' => $this->chatState->nextMessageId($roomId),
|
|
|
|
|
|
'room_id' => $roomId,
|
|
|
|
|
|
'from_user' => '系统',
|
|
|
|
|
|
'to_user' => $targetUsername,
|
|
|
|
|
|
'content' => $content,
|
|
|
|
|
|
'is_secret' => true,
|
|
|
|
|
|
'font_color' => $color,
|
|
|
|
|
|
'action' => '',
|
|
|
|
|
|
'sent_at' => now()->toDateTimeString(),
|
|
|
|
|
|
// 复用现有聊天 toast 机制,在右下角弹出职务变动提示。
|
|
|
|
|
|
'toast_notification' => [
|
|
|
|
|
|
'title' => $title,
|
|
|
|
|
|
'message' => $toastMessage,
|
|
|
|
|
|
'icon' => $icon,
|
|
|
|
|
|
'color' => $color,
|
|
|
|
|
|
'duration' => 10000,
|
|
|
|
|
|
],
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
$this->chatState->pushMessage($roomId, $msg);
|
|
|
|
|
|
broadcast(new MessageSent($roomId, $msg));
|
|
|
|
|
|
SaveMessageJob::dispatch($msg);
|
|
|
|
|
|
}
|
2026-02-28 23:44:38 +08:00
|
|
|
|
}
|