0 ? 'manual' : 'cron'; $deactivateDays = SettingService::getUserInactivityDeactivateDays(); $deleteDays = SettingService::getUserInactivityDeleteDays(); if ($deactivateDays <= 0) { $deleteDays = 0; } $privilegedUserIds = UserRepository::listPrivilegedUserIdsByPermissionKeys([ PermissionService::SETTINGS_UPDATE, PermissionService::TENANTS_UPDATE, ]); $result = [ 'ok' => true, 'locked' => false, 'error' => null, 'run_uuid' => $runUuid, 'deactivated_count' => 0, 'deleted_count' => 0, 'skipped_count' => 0, 'skipped_privileged_count' => count($privilegedUserIds), 'policy' => [ 'deactivate_days' => $deactivateDays, 'delete_days' => $deleteDays, ], 'duration_ms' => 0, ]; if (!self::acquireLock()) { $result['ok'] = false; $result['locked'] = true; $result['error'] = 'lock_not_acquired'; $result['duration_ms'] = self::durationMs($startedAt); return $result; } try { if ($deactivateDays > 0) { while (true) { $ids = UserRepository::listIdsForAutoDeactivate($deactivateDays, $privilegedUserIds, self::BATCH_SIZE); if (!$ids) { break; } $usersById = []; foreach ($ids as $id) { $user = UserRepository::find((int) $id); if ($user) { $usersById[(int) $id] = $user; } } $updated = UserRepository::setInactiveByIds( $ids, $actorUserId && $actorUserId > 0 ? $actorUserId : null ); if ($updated <= 0) { $result['ok'] = false; $result['error'] = 'deactivate_failed'; break; } $result['deactivated_count'] += $updated; UserRepository::bumpAuthzVersionByUserIds($ids); foreach ($ids as $id) { if (!isset($usersById[(int) $id])) { continue; } UserLifecycleAuditService::logDeactivate( $runUuid, $triggerType, $result['policy'], $actorUserId, $usersById[(int) $id], 'success', null ); } if (count($ids) < self::BATCH_SIZE) { break; } } } if ($deleteDays > 0) { while (true) { $ids = UserRepository::listIdsForAutoDelete($deleteDays, $privilegedUserIds, self::BATCH_SIZE); if (!$ids) { break; } foreach ($ids as $id) { $user = UserRepository::find((int) $id); if (!$user) { continue; } $auditId = UserLifecycleAuditService::logDeleteWithSnapshot( $runUuid, $triggerType, $result['policy'], $actorUserId, $user ); if (!$auditId) { $result['skipped_count']++; UserLifecycleAuditService::logDeleteFailure( $runUuid, $triggerType, $result['policy'], $actorUserId, $user, 'audit_snapshot_failed' ); continue; } $deleted = UserRepository::deleteByIds([(int) $id]); if ($deleted <= 0) { $result['skipped_count']++; UserLifecycleAuditService::updateStatus((int) $auditId, 'failed', 'delete_failed'); continue; } $result['deleted_count'] += $deleted; } if (count($ids) < self::BATCH_SIZE) { break; } } } } catch (\Throwable $exception) { $result['ok'] = false; $result['error'] = 'unexpected_error'; } finally { self::releaseLock(); $result['duration_ms'] = self::durationMs($startedAt); } return $result; } private static function acquireLock(): bool { $gotLock = DB::selectValue('select GET_LOCK(?, 0) as got_lock', self::LOCK_NAME); return (int) $gotLock === 1; } private static function releaseLock(): void { try { DB::selectValue('select RELEASE_LOCK(?) as released_lock', self::LOCK_NAME); } catch (\Throwable $exception) { // no-op } } private static function durationMs(float $startedAt): int { return (int) max(0, round((microtime(true) - $startedAt) * 1000)); } }