""" تشخیصِ «نصفه‌رها» (abandoned) با سیگنال قابل انجام نیست چون به گذر زمانِ بدون هیچ رویدادی نیاز داره، نه یک event مشخص؛ برای همین management commandه، نه receiver. باید periodic اجرا بشه (cron / celery beat)، مثلاً یک‌بار در روز: python manage.py mark_abandoned_node_progress آستانه از settings.NODE_PROGRESS_ABANDON_DAYS خونده می‌شه (پیش‌فرض ۱۴ روز). """ from django.conf import settings from django.core.management.base import BaseCommand from django.db import transaction from django.utils import timezone from content.models import NodeProgress from statuslog.signals import status_changed class Command(BaseCommand): help = 'رکوردهای NodeProgress در حال انجام که مدت‌ها فعالیتی نداشته‌اند را نصفه‌رها (abandoned) علامت می‌زند.' def handle(self, *args, **options): abandon_days = getattr(settings, 'NODE_PROGRESS_ABANDON_DAYS', 14) threshold = timezone.now() - timezone.timedelta(days=abandon_days) stale_qs = NodeProgress.objects.filter( status='in_progress', last_activity_at__lt=threshold, ) count = 0 # هر رکورد جدا اتمیک است (نه کل حلقه) تا خطای یک رکورد بقیه را rollback نکند. # id ها جدا گرفته می‌شوند و هر ردیف زیر select_for_update دوباره خوانده و # شرط abandoned بعد از قفل دوباره چک می‌شود، تا فعالیت هم‌زمان کاربر overwrite نشود. stale_ids = list(stale_qs.values_list('id', flat=True)) for progress_id in stale_ids: with transaction.atomic(): progress = NodeProgress.objects.select_for_update().get(pk=progress_id) if progress.status != 'in_progress' or progress.last_activity_at is None \ or progress.last_activity_at >= threshold: continue old_status = progress.status progress.status = 'abandoned' progress.save(update_fields=['status']) status_changed.send( sender='content.NodeProgress', object_id=progress.id, old_status=old_status, new_status='abandoned', event='inactivity_timeout', user_id=progress.user_id, ) count += 1 self.stdout.write(self.style.SUCCESS( f'{count} رکورد NodeProgress به abandoned تغییر کرد (آستانه: {abandon_days} روز).' ))