uchill/backend/apps/users/middleware/activity.py

118 lines
5.7 KiB
Python

"""
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
# Учёт дня активности для реферальной программы (не чаще 1 раза в день на пользователя)
today = now.date()
day_cache_key = f'referral_activity_day:{user.id}:{today}'
if not cache.get(day_cache_key):
try:
from apps.referrals.models import UserActivityDay
UserActivityDay.objects.get_or_create(user=user, date=today)
cache.set(day_cache_key, 1, timeout=86400 * 2)
except Exception:
pass
except Exception as e:
# Логируем ошибку, но не прерываем выполнение запроса
logger.error(f"Ошибка при обновлении last_activity для пользователя {user.id}: {e}", exc_info=True)