Files
chatroom/app/Services/AppointmentService.php
lkddi e21f049643 修复:勤务日榜在线时长统计虚高(142小时)+ UI文字调整
Bug修复:
- closeDutyLog 增加 whereDate 限制,只关闭今日日志,历史遗留记录置0,避免跨天时长被计入榜单
- tickDutyLog(ChatController/AutoSaveExp)找不到今日开放日志时不再盲目新建,避免同一 login_at 产生几十条重复记录后 SUM 叠加导致虚假142小时
- AppointmentService 撤职时 closeDutyLog 同步增加今日/历史遗留区分处理

UI调整:
- 登录页版权文字「飘落的流星」→「流星」
- 后台布局标题「飘落流星 控制台」→「控制台」
- 后台侧边栏移除非超管查看各模块时的「(只读)」标注
2026-03-01 22:55:55 +08:00

298 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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只结算今日历史遗留置0
$target->activePosition?->dutyLogs()
->whereNull('logout_at')
->whereDate('login_at', today())
->update([
'logout_at' => now(),
'duration_seconds' => DB::raw('TIMESTAMPDIFF(SECOND, login_at, NOW())'),
]);
// 历史遗留未关闭日志直接清零
$target->activePosition?->dutyLogs()
->whereNull('logout_at')
->whereDate('login_at', '<', today())
->update([
'logout_at' => DB::raw('login_at'),
'duration_seconds' => 0,
]);
// 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();
}
}