""" پاک کردنِ کامل‌ِ چند کاربر و همه‌ی داده‌شان در همه‌ی اپ‌ها. چرا لازم شد (۲۰۲۶-۱۰-۰۲): پیش از رسیدنِ کاربرِ واقعی، حساب‌هایی که تیم با شماره‌ی واقعیِ خودش ساخته بود باید پاک شوند تا همان شماره‌ها دوباره مسیرِ «کاربرِ تازه» را ببینند. پاک کردنِ ردیفِ `accounts.User` کافی نیست: اپ‌ها به کاربر فقط ارجاعِ سبک (`user_id`) دارند، نه FK، پس امتیاز، گفتگو، سفارش و اعلانِ او یتیم می‌ماند و کاربرِ تازه‌ای که همان شناسه را بگیرد آن‌ها را می‌دید (روی SQLite شناسه دوباره داده نمی‌شود، ولی روی Postgres پس از بازنشانیِ sequence ممکن است). مدل‌ها خودکار پیدا می‌شوند: هر مدلی که ستونِ `user_id` یا `referred_user_id` دارد. پس اپِ تازه بی‌ویرایشِ این فایل پوشش داده می‌شود. کدهای OTPِ همان شماره‌ها هم پاک می‌شوند تا سقفِ روزانه‌ی پیامکِ آن شماره از صفر شروع شود. پیش‌فرض فقط گزارش است و چیزی پاک نمی‌کند: python manage.py delete_users --phones 09121234567 09351234567 python manage.py delete_users --ids 17 22 --yes python manage.py delete_users --real-phones # هر کاربرِ شماره‌دار جز شماره‌های دمو `--real-phones` یعنی هر کاربری که شماره دارد و شماره‌اش در TEST_LOGIN_PHONES (حساب‌های دمو با کدِ ثابت) نیست. شماره‌ها از سرور بیرون نمی‌آیند؛ گزارش فقط چهار رقمِ اول و سه رقمِ آخر را نشان می‌دهد. مهمان‌ها (بی‌شماره) با این گزینه انتخاب نمی‌شوند. سفارشِ پرداخت‌شده سندِ مالی است؛ کاربری که سفارشِ «paid» دارد بدونِ --include-paid پاک نمی‌شود. """ from django.apps import apps from django.core.management.base import BaseCommand, CommandError from django.db import transaction from accounts.models import OTPCode, User from accounts.phone import normalize_phone USER_COLUMNS = ('user_id', 'referred_user_id') def user_scoped_models(): """[(model, column)] برای هر مدلی که به کاربر ارجاع می‌دهد، جز خودِ User.""" found = [] for model in apps.get_models(): if model is User: continue names = {f.attname for f in model._meta.concrete_fields} found += [(model, column) for column in USER_COLUMNS if column in names] return found def plan(user_ids): """{'app.Model.column': count} — آنچه پاک می‌شود، پیش از پاک شدن.""" counts = {} for model, column in user_scoped_models(): n = model.objects.filter(**{f'{column}__in': user_ids}).count() if n: counts[f'{model._meta.label}.{column}'] = n return counts def delete_users(user_ids): phones = list(User.objects.filter(pk__in=user_ids).exclude(phone=None).values_list('phone', flat=True)) with transaction.atomic(): for model, column in user_scoped_models(): model.objects.filter(**{f'{column}__in': user_ids}).delete() OTPCode.objects.filter(phone__in=phones).delete() User.objects.filter(pk__in=user_ids).delete() class Command(BaseCommand): help = 'کاربر و همه‌ی داده‌اش را در همه‌ی اپ‌ها پاک می‌کند (پیش‌فرض: فقط گزارش).' def add_arguments(self, parser): parser.add_argument('--phones', nargs='*', default=[]) parser.add_argument('--ids', nargs='*', type=int, default=[]) parser.add_argument('--real-phones', action='store_true', help='همه‌ی کاربرهای شماره‌دار جز شماره‌های TEST_LOGIN_PHONES.') parser.add_argument('--yes', action='store_true', help='واقعاً پاک کن.') parser.add_argument('--include-paid', action='store_true', help='کاربرِ دارای سفارشِ پرداخت‌شده را هم پاک کن.') def handle(self, *args, **options): phones = [normalize_phone(p) or p for p in options['phones']] users = list(User.objects.filter(phone__in=phones)) + list(User.objects.filter(pk__in=options['ids'])) if options['real_phones']: from django.conf import settings users += list(User.objects.exclude(phone=None).exclude(phone='') .exclude(phone__in=list(settings.TEST_LOGIN_PHONES))) users = list({u.pk: u for u in users}.values()) missing = set(phones) - {u.phone for u in users} if missing: self.stdout.write(f'این شماره‌ها کاربری ندارند: {len(missing)}') if not users: raise CommandError('کاربری برای پاک کردن پیدا نشد.') from payment.models import Order paid = set(Order.objects.filter(user_id__in=[u.pk for u in users], status='paid') .values_list('user_id', flat=True)) if paid and not options['include_paid']: raise CommandError(f'کاربرهای {sorted(paid)} سفارشِ پرداخت‌شده دارند؛ با --include-paid پاک می‌شوند.') ids = [u.pk for u in users] for u in users: phone = f'{u.phone[:4]}***{u.phone[-3:]}' if u.phone else 'مهمان' self.stdout.write(f' کاربرِ {u.pk} {phone} ساخته‌شده {u.created_at:%Y-%m-%d}') for label, n in sorted(plan(ids).items()): self.stdout.write(f' {label}: {n}') if not options['yes']: self.stdout.write(self.style.WARNING('فقط گزارش بود؛ برای پاک کردن --yes بدهید.')) return delete_users(ids) self.stdout.write(self.style.SUCCESS(f'{len(ids)} کاربر و همه‌ی داده‌شان پاک شدند.'))