"""استعلام وضعیت تحقق پیامک‌های ارسال‌شده (رسیده/نرسیده به گوشی). فقط برای provider وب‌سرویس 0098SMS معنا دارد؛ تنها آن provider با ``SendSMSWithID`` شناسهٔ پیامک (smsid) برمی‌گرداند و ``smsdeliveryState`` وضعیت تحویل را می‌دهد. برای سایر providerها دستور no-op است. """ import logging import signal import time from datetime import timedelta from django.core.management.base import BaseCommand from django.db.models import Q from django.utils import timezone from admin_panel.models import SMSProviderConfig, SMSQueue from integrations.sms.factory import get_sms_provider logger = logging.getLogger(__name__) class Command(BaseCommand): help = "استعلام وضعیت تحویل پیامک‌های ارسال‌شده از وب‌سرویس 0098SMS." def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._stop = False def add_arguments(self, parser): parser.add_argument("--once", action="store_true", help="یک دور پردازش و خروج") parser.add_argument("--sleep", type=float, default=30.0, help="فاصلهٔ بین دورها (ثانیه)") parser.add_argument("--batch", type=int, default=100, help="تعداد ردیف در هر دور") parser.add_argument( "--min-age", type=int, default=60, help="حداقل فاصله (ثانیه) از زمان ارسال پیش از نخستین استعلام", ) def handle(self, *args, **options): signal.signal(signal.SIGTERM, self._handle_signal) signal.signal(signal.SIGINT, self._handle_signal) once = options["once"] sleep_seconds = options["sleep"] self._emit("استعلام دلیوری پیامک آغاز شد.", style=self.style.SUCCESS) while not self._stop: try: processed = self._drain_batch( options["batch"], options["min_age"], ) except Exception: logger.exception("خطای غیرمنتظره در یک دور از استعلام دلیوری؛ دور بعد ادامه می‌دهد.") processed = 0 if once: break if processed == 0: self._sleep_interruptible(sleep_seconds) self._emit("استعلام دلیوری پیامک متوقف شد.", style=self.style.SUCCESS) # ------------------------------------------------------------------ helpers def _handle_signal(self, signum, frame): self._stop = True def _emit(self, text, *, style=None, err=False): """چاپ با تحمل کدگذاری محدود کنسول (مثل worker پیامک).""" stream = self.stderr if err else self.stdout style = style or (lambda value: value) for payload in (text, text.encode("ascii", "replace").decode("ascii")): try: stream.write(style(payload)) return except (UnicodeEncodeError, ValueError, OSError): continue def _sleep_interruptible(self, seconds): deadline = time.monotonic() + max(0, seconds) while not self._stop and time.monotonic() < deadline: time.sleep(min(0.5, deadline - time.monotonic())) def _drain_batch(self, batch, min_age): config = SMSProviderConfig.load() if config.provider != "sms0098ws": # smsid تنها متعلق به این provider است؛ استعلام با بقیه بی‌معناست. logger.info("provider فعلی رهگیری دلیوری ندارد؛ استعلام رد شد.") return 0 now = timezone.now() cutoff = now - timedelta(seconds=max(0, min_age)) # پیامک‌های ارسال‌شده که شناسه دارند و وضعیت‌شان هنوز نهایی نشده است. query = SMSQueue.objects.filter( Q(status=SMSQueue.Status.SENT, provider_id__gt="", sent_at__lte=cutoff), Q(delivery_checked_at__isnull=True) | Q(delivery_status__in=["", SMSQueue.DeliveryStatus.PENDING, SMSQueue.DeliveryStatus.UNKNOWN]), ) try: provider = get_sms_provider(config) except Exception: logger.exception("ساخت provider پیامک ناموفق بود؛ این دور رد شد.") return 0 processed = 0 for row in query.order_by("sent_at")[:batch]: if self._stop: break result = provider.get_delivery_status(row.provider_id) if result is None: # خطای ارتباط/تنظیمات: ردیف را رد کن تا دور بعد دوباره تلاش شود. continue row.delivery_status = result.status row.delivery_checked_at = timezone.now() row.save(update_fields=["delivery_status", "delivery_checked_at", "updated_at"]) processed += 1 self._emit( f" دلیوری #{row.pk} ({row.provider_id}): {result.status} " f"{'(نهایی)' if result.is_final else '(در انتظار)'}", ) return processed