- 任命/撤销事件增加 type 字段区分类型 - 任命:全屏礼花 + 紫色弹窗 + 紫色系统消息 - 撤销:灰色弹窗 + 灰色系统消息,无礼花 - 消息分发:操作者/被操作者显示在私聊面板,其他人显示在公屏 - 系统消息加随机鼓励语(各5条轮换) - ChatStateService 修复 Redis key 前缀扫描问题(getAllActiveRoomIds) - 用户名片折叠优化:管理员视野、职务履历均可折叠 - 管理操作 + 职务操作合并为「🔧 管理操作」折叠区 - 悄悄话改为「🎁 送礼物」按钮,礼物面板内联展开
288 lines
10 KiB
PHP
288 lines
10 KiB
PHP
<?php
|
||
|
||
/**
|
||
* 文件功能:职务任命服务
|
||
* 处理职务系统的核心业务逻辑:任命、撤销、权限校验
|
||
* 所有权限操作均写入 position_authority_logs 留存审计
|
||
*
|
||
* @author ChatRoom Laravel
|
||
*
|
||
* @version 1.0.0
|
||
*/
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Position;
|
||
use App\Models\PositionAuthorityLog;
|
||
use App\Models\User;
|
||
use App\Models\UserPosition;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
class AppointmentService
|
||
{
|
||
/**
|
||
* 获取用户当前在职记录(无则返回 null)
|
||
*/
|
||
public function getActivePosition(User $user): ?UserPosition
|
||
{
|
||
return UserPosition::query()
|
||
->where('user_id', $user->id)
|
||
->where('is_active', true)
|
||
->with(['position.department', 'position.appointablePositions'])
|
||
->first();
|
||
}
|
||
|
||
/**
|
||
* 校验操作人是否有权将目标用户任命到指定职务
|
||
*
|
||
* id=1 超级管理员绕过所有要目校验,可直接任命任意职务。
|
||
*
|
||
* @return array{ok: bool, message: string}
|
||
*/
|
||
public function validateAppoint(User $operator, User $target, Position $targetPosition): array
|
||
{
|
||
// 超级管理员(id=1)特权:跳过职务和白名单校验,只检查被任命人是否已有职务
|
||
if ($operator->id === 1) {
|
||
$existingPosition = $this->getActivePosition($target);
|
||
if ($existingPosition) {
|
||
$currentName = $existingPosition->position->name;
|
||
|
||
return ['ok' => false, 'message' => "【{$target->username}】当前已担任【{$currentName}】,请先撤销其职务再重新任命。"];
|
||
}
|
||
|
||
return ['ok' => true, 'message' => '超级管理员直接授权'];
|
||
}
|
||
|
||
// 操作人必须有在职职务
|
||
$operatorPosition = $this->getActivePosition($operator);
|
||
if (! $operatorPosition) {
|
||
return ['ok' => false, 'message' => '您当前无在职职务,无法进行任命操作。'];
|
||
}
|
||
|
||
// 校验任命白名单:目标职务是否在操作人职务的可任命列表内
|
||
$isAllowed = $operatorPosition->position
|
||
->appointablePositions()
|
||
->where('positions.id', $targetPosition->id)
|
||
->exists();
|
||
|
||
if (! $isAllowed) {
|
||
return ['ok' => false, 'message' => "您的职务无权任命【{$targetPosition->name}】职位。"];
|
||
}
|
||
|
||
// 检查目标职务是否已满员
|
||
if ($targetPosition->isFull()) {
|
||
return ['ok' => false, 'message' => "【{$targetPosition->name}】职位人数已满,无法继续任命。"];
|
||
}
|
||
|
||
// 检查被任命人是否已有在职职务
|
||
$existingPosition = $this->getActivePosition($target);
|
||
if ($existingPosition) {
|
||
$currentName = $existingPosition->position->name;
|
||
|
||
return ['ok' => false, 'message' => "【{$target->username}】当前已担任【{$currentName}】,请先撤销其职务再重新任命。"];
|
||
}
|
||
|
||
return ['ok' => true, 'message' => '校验通过'];
|
||
}
|
||
|
||
/**
|
||
* 执行任命操作
|
||
* 任命成功后自动同步 user_level 并写入权限日志
|
||
*
|
||
* @return array{ok: bool, message: string, userPosition?: UserPosition}
|
||
*/
|
||
public function appoint(User $operator, User $target, Position $targetPosition, ?string $remark = null): array
|
||
{
|
||
// 权限校验
|
||
$validation = $this->validateAppoint($operator, $target, $targetPosition);
|
||
if (! $validation['ok']) {
|
||
return $validation;
|
||
}
|
||
|
||
// id=1 超级管理员无需在职职务,直接任命
|
||
$operatorPosition = $operator->id === 1 ? null : $this->getActivePosition($operator);
|
||
|
||
DB::transaction(function () use ($operator, $target, $targetPosition, $remark, $operatorPosition, &$userPosition) {
|
||
// 创建任职记录
|
||
$userPosition = UserPosition::create([
|
||
'user_id' => $target->id,
|
||
'position_id' => $targetPosition->id,
|
||
'appointed_by_user_id' => $operator->id,
|
||
'appointed_at' => now(),
|
||
'remark' => $remark,
|
||
'is_active' => true,
|
||
]);
|
||
|
||
// 同步 user_level
|
||
$target->update(['user_level' => $targetPosition->level]);
|
||
|
||
// 写入权限操作日志
|
||
$this->logAuthority(
|
||
operator: $operator,
|
||
operatorPosition: $operatorPosition,
|
||
actionType: 'appoint',
|
||
target: $target,
|
||
targetPosition: $targetPosition,
|
||
remark: $remark
|
||
);
|
||
});
|
||
|
||
return [
|
||
'ok' => true,
|
||
'message' => "已成功将【{$target->username}】任命为【{$targetPosition->name}】。",
|
||
'userPosition' => $userPosition,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 校验操作人是否有权撤销目标用户的职务
|
||
*
|
||
* id=1 超级管理员可直接撤销任意职务。
|
||
*
|
||
* @return array{ok: bool, message: string}
|
||
*/
|
||
public function validateRevoke(User $operator, User $target): array
|
||
{
|
||
// 超级管理员(id=1)特权:跳过白名单校验,直接撤销任意职务
|
||
if ($operator->id === 1) {
|
||
$targetPosition = $this->getActivePosition($target);
|
||
if (! $targetPosition) {
|
||
return ['ok' => false, 'message' => "【{$target->username}】当前没有在职职务。"];
|
||
}
|
||
|
||
return ['ok' => true, 'message' => '超级管理员直接授权'];
|
||
}
|
||
|
||
// 操作人必须有在职职务
|
||
$operatorPosition = $this->getActivePosition($operator);
|
||
if (! $operatorPosition) {
|
||
return ['ok' => false, 'message' => '您当前无在职职务,无法进行撤职操作。'];
|
||
}
|
||
|
||
// 被撤销人必须有在职职务
|
||
$targetPosition = $this->getActivePosition($target);
|
||
if (! $targetPosition) {
|
||
return ['ok' => false, 'message' => "【{$target->username}】当前没有在职职务。"];
|
||
}
|
||
|
||
// 操作人不能撤销自己
|
||
if ($operator->id === $target->id) {
|
||
return ['ok' => false, 'message' => '不能撤销自己的职务。'];
|
||
}
|
||
|
||
// 操作人的任命白名单中必须包含目标职务(即有权任命该职务,也就有权撤销)
|
||
$isAllowed = $operatorPosition->position
|
||
->appointablePositions()
|
||
->where('positions.id', $targetPosition->position_id)
|
||
->exists();
|
||
|
||
if (! $isAllowed) {
|
||
return ['ok' => false, 'message' => "您的职务无权撤销【{$targetPosition->position->name}】职位的人员。"];
|
||
}
|
||
|
||
return ['ok' => true, 'message' => '校验通过'];
|
||
}
|
||
|
||
/**
|
||
* 执行撤销职务操作
|
||
* 撤销后 user_level 归 1,并写入权限日志
|
||
*
|
||
* @return array{ok: bool, message: string}
|
||
*/
|
||
public function revoke(User $operator, User $target, ?string $remark = null): array
|
||
{
|
||
// 权限校验
|
||
$validation = $this->validateRevoke($operator, $target);
|
||
if (! $validation['ok']) {
|
||
return $validation;
|
||
}
|
||
|
||
$operatorPosition = $this->getActivePosition($operator);
|
||
$targetUP = $this->getActivePosition($target);
|
||
|
||
DB::transaction(function () use ($operator, $target, $remark, $operatorPosition, $targetUP) {
|
||
// 撤销在职记录
|
||
$targetUP->update([
|
||
'is_active' => false,
|
||
'revoked_at' => now(),
|
||
'revoked_by_user_id' => $operator->id,
|
||
]);
|
||
|
||
// 关闭尚未结束的 duty_log
|
||
$target->activePosition?->dutyLogs()
|
||
->whereNull('logout_at')
|
||
->update([
|
||
'logout_at' => now(),
|
||
'duration_seconds' => DB::raw('TIMESTAMPDIFF(SECOND, login_at, NOW())'),
|
||
]);
|
||
|
||
// user_level 归 1(由系统经验值自然升级机制重新成长)
|
||
$target->update(['user_level' => 1]);
|
||
|
||
// 写入权限操作日志
|
||
$this->logAuthority(
|
||
operator: $operator,
|
||
operatorPosition: $operatorPosition,
|
||
actionType: 'revoke',
|
||
target: $target,
|
||
targetPosition: $targetUP->position,
|
||
remark: $remark
|
||
);
|
||
});
|
||
|
||
return [
|
||
'ok' => true,
|
||
'message' => "已成功撤销【{$target->username}】的【{$targetUP->position->name}】职务,其等级已归 1。",
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 记录权限操作日志(各类管理操作公共调用)
|
||
*
|
||
* @param string $actionType 操作类型(appoint/revoke/reward/warn/kick/mute/banip/other)
|
||
*/
|
||
public function logAuthority(
|
||
User $operator,
|
||
?UserPosition $operatorPosition,
|
||
string $actionType,
|
||
User $target,
|
||
?Position $targetPosition = null,
|
||
?int $amount = null,
|
||
?string $remark = null,
|
||
): void {
|
||
// 无在职职务的操作不记录(普通管理员通过 user_level 操作不进此表)
|
||
if (! $operatorPosition) {
|
||
return;
|
||
}
|
||
|
||
PositionAuthorityLog::create([
|
||
'user_id' => $operator->id,
|
||
'user_position_id' => $operatorPosition->id,
|
||
'action_type' => $actionType,
|
||
'target_user_id' => $target->id,
|
||
'target_position_id' => $targetPosition?->id,
|
||
'amount' => $amount,
|
||
'remark' => $remark,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 获取视图用:操作人有权任命的职务列表(用于后台/弹窗任命下拉选择)
|
||
*
|
||
* @return \Illuminate\Database\Eloquent\Collection<int, Position>
|
||
*/
|
||
public function getAppointablePositions(User $operator)
|
||
{
|
||
$operatorPosition = $this->getActivePosition($operator);
|
||
if (! $operatorPosition) {
|
||
return collect();
|
||
}
|
||
|
||
return $operatorPosition->position
|
||
->appointablePositions()
|
||
->with('department')
|
||
->orderByDesc('rank')
|
||
->get();
|
||
}
|
||
}
|