2026-02-28 23:44:38 +08:00
|
|
|
|
<?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,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-03-01 22:55:55 +08:00
|
|
|
|
// 关闭尚未结束的 duty_log(只结算今日,历史遗留置0)
|
2026-02-28 23:44:38 +08:00
|
|
|
|
$target->activePosition?->dutyLogs()
|
|
|
|
|
|
->whereNull('logout_at')
|
2026-03-01 22:55:55 +08:00
|
|
|
|
->whereDate('login_at', today())
|
2026-02-28 23:44:38 +08:00
|
|
|
|
->update([
|
2026-03-01 22:55:55 +08:00
|
|
|
|
'logout_at' => now(),
|
2026-02-28 23:44:38 +08:00
|
|
|
|
'duration_seconds' => DB::raw('TIMESTAMPDIFF(SECOND, login_at, NOW())'),
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-03-01 22:55:55 +08:00
|
|
|
|
// 历史遗留未关闭日志直接清零
|
|
|
|
|
|
$target->activePosition?->dutyLogs()
|
|
|
|
|
|
->whereNull('logout_at')
|
|
|
|
|
|
->whereDate('login_at', '<', today())
|
|
|
|
|
|
->update([
|
|
|
|
|
|
'logout_at' => DB::raw('login_at'),
|
|
|
|
|
|
'duration_seconds' => 0,
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
2026-02-28 23:44:38 +08:00
|
|
|
|
// 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|