Files
khatibi/admin_panel/management/commands/cleanup_visit_dedup.py
T
2026-10-20 19:02:34 +03:30

82 lines
4.3 KiB
Python

"""پاک‌سازی کلیدهای «آلوده» دفتر یکتاسازی یادآوری بازدید.
چرا لازم است؟ کلید یکتاسازیِ یادآوری بازدید، در نسخهٔ قدیمی ``visit:<id>:<شمارهٔ چرخه>``
بود که شمارهٔ چرخه با تقسیم روز بر یک عدد ثابت محاسبه می‌شد. وقتی آن عدد صفر می‌شد،
عددهای بی‌معنا (مثل ``visit:1601:24663``) ساخته و برای همیشه در ``SMSDedup`` ثبت
می‌شدند؛ نتیجه‌اش قفل دائمی یادآوری برای همان مشتری بود.
قاعدهٔ جدید کلید را با «موعد تقویمی» می‌سازد: ``visit:<id>:<YYYY-MM>``. کلیدهای قدیمی
هرگز با کلید جدید برخورد نمی‌کنند، بنابراین بی‌ضرر ولی بازدارنده‌اند.
این دستور **فقط** کلیدهای بی‌معنا/قدیمیِ دفتر را حذف می‌کند و هیچ مشتری، اقدام یا
پیامکی را تغییر نمی‌دهد.
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:<id>:<عدد>) حذف شوند، "
"نه فقط عددهای بی‌معنا.")
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:,} کلید حذف شد (فقط از دفتر یکتاسازی)."
))