chore: initial import for test contour with k3s CI

This commit is contained in:
sova-bootstrap
2026-05-28 12:09:28 +03:00
commit d77d0a872f
423 changed files with 35401 additions and 0 deletions
+318
View File
@@ -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()
));
}
}
}