chore: initial import for test contour with k3s CI
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler;
|
||||
|
||||
class UserCleanupService
|
||||
{
|
||||
private $userRepository;
|
||||
private $entityManager;
|
||||
private $logger;
|
||||
private $cookieLifetime;
|
||||
private $sessionHandler;
|
||||
|
||||
public function __construct(
|
||||
UserRepository $userRepository,
|
||||
EntityManagerInterface $entityManager,
|
||||
LoggerInterface $logger,
|
||||
array $sessionStorageOptions = [],
|
||||
$sessionHandler = null
|
||||
) {
|
||||
$this->userRepository = $userRepository;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->logger = $logger;
|
||||
$this->cookieLifetime = $sessionStorageOptions['cookie_lifetime'] ?? 0;
|
||||
$this->sessionHandler = $sessionHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет неактивных пользователей с только ROLE_USER после истечения времени жизни кукисов
|
||||
*
|
||||
* @return int Количество удаленных пользователей
|
||||
*/
|
||||
public function cleanupExpiredUsers(): int
|
||||
{
|
||||
// Если cookie_lifetime = 0, используем время жизни сессии по умолчанию (1 час)
|
||||
// Иначе используем указанное время жизни кукисов
|
||||
$sessionLifetime = $this->cookieLifetime > 0
|
||||
? $this->cookieLifetime
|
||||
: 3600; // 1 час по умолчанию для session cookies
|
||||
|
||||
// Вычисляем дату, до которой должна быть последняя активность пользователя
|
||||
$beforeDate = new \DateTime();
|
||||
$beforeDate->modify("-{$sessionLifetime} seconds");
|
||||
|
||||
$usersToDelete = $this->userRepository->findUsersToCleanup($beforeDate);
|
||||
$deletedCount = 0;
|
||||
|
||||
$this->logger->info(sprintf(
|
||||
'Найдено пользователей для проверки: %d (время жизни сессии: %d секунд, дата отсечки: %s)',
|
||||
count($usersToDelete),
|
||||
$sessionLifetime,
|
||||
$beforeDate->format('Y-m-d H:i:s')
|
||||
));
|
||||
|
||||
foreach ($usersToDelete as $user) {
|
||||
// Проверяем, что у пользователя только ROLE_USER
|
||||
// Если массив ролей пуст, значит у пользователя только ROLE_USER (который добавляется автоматически)
|
||||
$actualRoles = $user->getActualRoles();
|
||||
$hasOnlyUserRole = empty($actualRoles);
|
||||
|
||||
$this->logger->debug(sprintf(
|
||||
'Проверка пользователя ID %d: роли=%s, только ROLE_USER=%s',
|
||||
$user->getId(),
|
||||
json_encode($actualRoles),
|
||||
$hasOnlyUserRole ? 'да' : 'нет'
|
||||
));
|
||||
|
||||
if ($hasOnlyUserRole) {
|
||||
// Определяем дату для логирования
|
||||
$activityDate = $user->getLastActivityAt() ?? $user->getCreatedAt();
|
||||
$activityDateStr = $activityDate ? $activityDate->format('Y-m-d H:i:s') : 'N/A';
|
||||
|
||||
try {
|
||||
// Удаляем сессии пользователя из Redis перед удалением пользователя
|
||||
$this->cleanupUserSessions($user);
|
||||
|
||||
$this->deleteUserAndRelatedData($user);
|
||||
$deletedCount++;
|
||||
$this->logger->info(sprintf(
|
||||
'Удален неактивный пользователь с ID %d (uid: %d, email: %s, последняя активность: %s)',
|
||||
$user->getId(),
|
||||
$user->getUid(),
|
||||
$user->getEmail() ?? 'N/A',
|
||||
$activityDateStr
|
||||
));
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error(sprintf(
|
||||
'Ошибка при удалении пользователя с ID %d: %s',
|
||||
$user->getId(),
|
||||
$e->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем старые сессии из Redis
|
||||
$this->cleanupOldRedisSessions($sessionLifetime);
|
||||
|
||||
return $deletedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет пользователя и все связанные данные
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
private function deleteUserAndRelatedData(User $user): void
|
||||
{
|
||||
// Удаляем связанные данные Calltouch (если есть)
|
||||
$this->deleteCalltouchData($user);
|
||||
|
||||
// Удаляем сам пользователя
|
||||
$this->entityManager->remove($user);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает сессии пользователя из Redis
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
private function cleanupUserSessions(User $user): void
|
||||
{
|
||||
if (!$this->sessionHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$redis = $this->getRedisClient();
|
||||
if (!$redis) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ищем все сессии, которые могут принадлежать пользователю
|
||||
// В Symfony сессии обычно хранятся с префиксом PHPSESSID или sess_
|
||||
$patterns = ['PHPSESSID_*', 'sess_*', '*_symfony_*'];
|
||||
$deletedSessions = 0;
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
$keys = $redis->keys($pattern);
|
||||
if (!$keys) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($keys as $key) {
|
||||
try {
|
||||
$sessionData = $redis->get($key);
|
||||
if ($sessionData) {
|
||||
// Пытаемся найти user_id в данных сессии
|
||||
// Сессии Symfony обычно содержат сериализованные данные
|
||||
if (strpos($sessionData, '_security_main') !== false ||
|
||||
strpos($sessionData, (string)$user->getId()) !== false ||
|
||||
strpos($sessionData, (string)$user->getUid()) !== false) {
|
||||
$redis->del($key);
|
||||
$deletedSessions++;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Игнорируем ошибки при проверке отдельных ключей
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($deletedSessions > 0) {
|
||||
$this->logger->info(sprintf(
|
||||
'Удалено %d сессий из Redis для пользователя %d',
|
||||
$deletedSessions,
|
||||
$user->getId()
|
||||
));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning(sprintf(
|
||||
'Ошибка при очистке сессий пользователя %d из Redis: %s',
|
||||
$user->getId(),
|
||||
$e->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает старые сессии из Redis
|
||||
*
|
||||
* @param int $sessionLifetime Время жизни сессии в секундах
|
||||
*/
|
||||
private function cleanupOldRedisSessions(int $sessionLifetime): void
|
||||
{
|
||||
if (!$this->sessionHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$redis = $this->getRedisClient();
|
||||
if (!$redis) {
|
||||
return;
|
||||
}
|
||||
|
||||
$patterns = ['PHPSESSID_*', 'sess_*', '*_symfony_*'];
|
||||
$deletedCount = 0;
|
||||
$now = time();
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
$keys = $redis->keys($pattern);
|
||||
if (!$keys) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($keys as $key) {
|
||||
try {
|
||||
$ttl = $redis->ttl($key);
|
||||
// Если TTL отрицательный (ключ не имеет TTL) или больше времени жизни сессии, пропускаем
|
||||
if ($ttl < 0 || $ttl > $sessionLifetime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Проверяем, не истекла ли сессия
|
||||
$lastAccess = $now - $ttl;
|
||||
if ($lastAccess > $sessionLifetime) {
|
||||
$redis->del($key);
|
||||
$deletedCount++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Игнорируем ошибки при проверке отдельных ключей
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($deletedCount > 0) {
|
||||
$this->logger->info(sprintf(
|
||||
'Удалено %d старых сессий из Redis',
|
||||
$deletedCount
|
||||
));
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning(sprintf(
|
||||
'Ошибка при очистке старых сессий из Redis: %s',
|
||||
$e->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает Redis клиент из session handler
|
||||
*
|
||||
* @return \Redis|\Predis\Client|null
|
||||
*/
|
||||
private function getRedisClient()
|
||||
{
|
||||
if (!$this->sessionHandler) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Пытаемся получить Redis клиент из различных типов session handlers
|
||||
if ($this->sessionHandler instanceof \Redis) {
|
||||
return $this->sessionHandler;
|
||||
}
|
||||
|
||||
if ($this->sessionHandler instanceof \Predis\Client) {
|
||||
return $this->sessionHandler;
|
||||
}
|
||||
|
||||
// Для RedisSessionHandler из Symfony
|
||||
if ($this->sessionHandler instanceof RedisSessionHandler) {
|
||||
$reflection = new \ReflectionClass($this->sessionHandler);
|
||||
if ($reflection->hasProperty('redis')) {
|
||||
$property = $reflection->getProperty('redis');
|
||||
$property->setAccessible(true);
|
||||
return $property->getValue($this->sessionHandler);
|
||||
}
|
||||
}
|
||||
|
||||
// Пытаемся вызвать метод getRedis, если он существует
|
||||
if (method_exists($this->sessionHandler, 'getRedis')) {
|
||||
return $this->sessionHandler->getRedis();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->warning(sprintf(
|
||||
'Не удалось получить Redis клиент: %s',
|
||||
$e->getMessage()
|
||||
));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет данные Calltouch, связанные с пользователем
|
||||
*
|
||||
* @param User $user
|
||||
*/
|
||||
private function deleteCalltouchData(User $user): void
|
||||
{
|
||||
try {
|
||||
// Проверяем наличие связи с Calltouch через миграции
|
||||
// Если есть таблица calltouch с внешним ключом на users.id
|
||||
$connection = $this->entityManager->getConnection();
|
||||
|
||||
// Удаляем записи calltouch, связанные с пользователем
|
||||
$connection->executeStatement(
|
||||
'DELETE FROM calltouch WHERE user_id = :userId',
|
||||
['userId' => $user->getId()]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// Игнорируем ошибки, если таблица не существует или нет связи
|
||||
$this->logger->warning(sprintf(
|
||||
'Не удалось удалить данные Calltouch для пользователя %d: %s',
|
||||
$user->getId(),
|
||||
$e->getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user