""" Middleware для отслеживания активности пользователей. Обновляет last_activity при каждом запросе для более точного определения онлайн статуса. """ import logging from django.utils import timezone from django.core.cache import cache logger = logging.getLogger(__name__) class UpdateLastActivityMiddleware: """ Middleware для обновления last_activity пользователя при каждом запросе. Использует кэширование для уменьшения нагрузки на базу данных: - Обновляет last_activity не чаще чем раз в 30 секунд для каждого пользователя - Использует Redis cache для хранения времени последнего обновления - Исключает статические файлы, media файлы и служебные эндпоинты из отслеживания """ # Пути, которые не должны обновлять last_activity EXCLUDED_PATHS = [ '/media/', '/static/', '/admin/jsi18n/', '/api/health/', '/api/docs/', '/favicon.ico', '/robots.txt', ] # HTTP методы, которые должны обновлять last_activity INCLUDED_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] # Интервал обновления в секундах (минимальное время между обновлениями для одного пользователя) # Обновляем не чаще чем раз в 30 секунд для баланса между точностью и производительностью UPDATE_INTERVAL = 30 # 30 секунд def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # Проверяем, нужно ли обновлять last_activity if self.should_update_activity(request): self.update_user_activity(request) response = self.get_response(request) return response def should_update_activity(self, request): """ Проверяет, нужно ли обновлять last_activity для этого запроса. """ # Обновляем только для авторизованных пользователей if not request.user.is_authenticated: return False # Проверяем метод запроса if request.method not in self.INCLUDED_METHODS: return False # Исключаем определенные пути path = request.path if any(path.startswith(excluded) for excluded in self.EXCLUDED_PATHS): return False # Проверяем, не обновляли ли мы недавно для этого пользователя cache_key = f'user_activity_update:{request.user.id}' last_update = cache.get(cache_key) if last_update: # Если прошло меньше UPDATE_INTERVAL секунд, не обновляем time_since_update = (timezone.now() - last_update).total_seconds() if time_since_update < self.UPDATE_INTERVAL: return False return True def update_user_activity(self, request): """ Обновляет last_activity для пользователя. Использует кэширование для уменьшения нагрузки на базу данных. """ user = request.user now = timezone.now() try: cache_key = f'user_activity_update:{user.id}' # Обновляем last_activity в базе данных # Используем update для атомарного обновления без загрузки объекта from apps.users.models import User User.objects.filter(id=user.id).update(last_activity=now) # Обновляем кэш для отслеживания последнего обновления # Время жизни кэша в 2 раза больше интервала обновления для надежности cache.set(cache_key, now, timeout=self.UPDATE_INTERVAL * 2) # Обновляем объект пользователя в запросе для текущего запроса # Это позволяет использовать обновленное значение в текущем запросе user.last_activity = now except Exception as e: # Логируем ошибку, но не прерываем выполнение запроса logger.error(f"Ошибка при обновлении last_activity для пользователя {user.id}: {e}", exc_info=True)