118 lines
5.7 KiB
Python
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)
|