"""پاک‌سازی کلیدهای «آلوده» دفتر یکتاسازی یادآوری بازدید. چرا لازم است؟ کلید یکتاسازیِ یادآوری بازدید، در نسخهٔ قدیمی ``visit::<شمارهٔ چرخه>`` بود که شمارهٔ چرخه با تقسیم روز بر یک عدد ثابت محاسبه می‌شد. وقتی آن عدد صفر می‌شد، عددهای بی‌معنا (مثل ``visit:1601:24663``) ساخته و برای همیشه در ``SMSDedup`` ثبت می‌شدند؛ نتیجه‌اش قفل دائمی یادآوری برای همان مشتری بود. قاعدهٔ جدید کلید را با «موعد تقویمی» می‌سازد: ``visit::``. کلیدهای قدیمی هرگز با کلید جدید برخورد نمی‌کنند، بنابراین بی‌ضرر ولی بازدارنده‌اند. این دستور **فقط** کلیدهای بی‌معنا/قدیمیِ دفتر را حذف می‌کند و هیچ مشتری، اقدام یا پیامکی را تغییر نمی‌دهد. python manage.py cleanup_visit_dedup --dry-run # فقط گزارش python manage.py cleanup_visit_dedup # حذف کلیدهای بی‌معنا (cycle > حد) python manage.py cleanup_visit_dedup --all-legacy # حذف همهٔ کلیدهای قالب قدیمی """ import re from django.core.management.base import BaseCommand from django.db import transaction from ...models import SMSDedup # سقف منطقی شمارهٔ چرخه؛ هر عدد بزرگ‌تر از این، نتیجهٔ باگ تقسیم قدیمی است. MAX_SANE_CYCLE = 50 LEGACY_PATTERN = re.compile(r"^visit:(\d+):(\d+)$") NEW_PATTERN = re.compile(r"^visit:(\d+):(\d{4}-\d{2})$") class Command(BaseCommand): help = "حذف کلیدهای آلوده/قدیمی دفتر یکتاسازی یادآوری بازدید (بدون تغییر دادهٔ مشتری)." def add_arguments(self, parser): parser.add_argument("--dry-run", action="store_true", help="فقط گزارش؛ چیزی حذف نمی‌شود.") parser.add_argument("--all-legacy", action="store_true", help="همهٔ کلیدهای قالب قدیمی (visit::<عدد>) حذف شوند، " "نه فقط عددهای بی‌معنا.") def handle(self, *args, **options): dry_run = options["dry_run"] all_legacy = options["all_legacy"] absurd, sane_legacy, current = [], [], [] for key in SMSDedup.objects.filter(key__startswith="visit:").values_list( "key", flat=True ).iterator(): if NEW_PATTERN.match(key): current.append(key) continue match = LEGACY_PATTERN.match(key) if match: cycle = int(match.group(2)) (absurd if cycle > MAX_SANE_CYCLE else sane_legacy).append(key) else: absurd.append(key) doomed = absurd + (sane_legacy if all_legacy else []) self.stdout.write(self.style.MIGRATE_LABEL("گزارش کلیدهای یادآوری بازدید:")) self.stdout.write(f" کلیدهای قالب جدید (موعد تقویمی): {len(current):,}") self.stdout.write(f" کلیدهای بی‌معنا (چرخه > {MAX_SANE_CYCLE} یا ناشناخته): {len(absurd):,}") self.stdout.write(f" کلیدهای قدیمیِ سالم (چرخه ≤ {MAX_SANE_CYCLE}): {len(sane_legacy):,}") if absurd: self.stdout.write(" نمونهٔ کلیدهای بی‌معنا: " + ", ".join(absurd[:5])) self.stdout.write(f" قابل حذف در این اجرا: {len(doomed):,}") if dry_run: self.stdout.write(self.style.WARNING( "\n[شبیه‌سازی] هیچ کلیدی حذف نشد." )) return if not doomed: self.stdout.write(self.style.SUCCESS("\nانجام شد: چیزی برای حذف نبود.")) return with transaction.atomic(): deleted, _ = SMSDedup.objects.filter(key__in=doomed).delete() self.stdout.write(self.style.SUCCESS( f"\nانجام شد: {deleted:,} کلید حذف شد (فقط از دفتر یکتاسازی)." ))