from django.db import models class User(models.Model): """ کاربر اصلی سیستم (مهمان یا ثبت‌نام‌شده با شماره). توجه: این مدل، AUTH_USER_MODEL جنگو نیست؛ یک مدل بیزنسی مستقله که با TadrisinoTokenAuthentication سفارشی احراز هویت می‌شه. grade_id فقط یک ارجاع سبک (بدون FK واقعی) به content.Grade هست؛ این اپ عمداً هیچ‌وقت FK واقعی به مدل اپ دیگه نمی‌زنه. """ phone = models.CharField(max_length=15, unique=True, null=True, blank=True, db_index=True) device_id = models.CharField(max_length=64, null=True, blank=True, db_index=True) name = models.CharField(max_length=100, blank=True) avatar = models.CharField(max_length=100, blank=True) is_guest = models.BooleanField(default=True) grade_id = models.IntegerField(null=True, blank=True) # ارجاع سبک به content.Grade created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-created_at'] constraints = [ # 🔒 اصلاح race condition: قبلاً device_id هیچ قید یکتاییِ سطح # DB نداشت، پس User.objects.get_or_create(device_id=...) در # GuestSessionView در برابر دو درخواستِ هم‌زمان با همون # device_id، می‌تونست دو کاربرِ مهمانِ جدا بسازه (هر دو create # می‌شدن چون هیچ‌کدوم قبل از دیگری commit نشده بود که get بعدی # پیداش کنه). این UniqueConstraint (فقط برای کاربرانِ مهمان و # device_id غیرخالی — کاربرانِ ثبت‌نام‌شده با phone می‌تونن # device_id مشترک/خالی داشته باشن) کافیه؛ چون Django's # get_or_create از قبل خودش IntegrityError روی همین constraint # رو می‌گیره و رکورد موجود رو دوباره get می‌کنه (نگاه کنید به # پیاده‌سازی _create_object_from_params در Django)، پس نیازی به # تغییر GuestSessionView نبود. models.UniqueConstraint( fields=['device_id'], condition=models.Q(is_guest=True) & ~models.Q(device_id=None) & ~models.Q(device_id=''), name='uniq_guest_device_id', ), ] def __str__(self): return self.name or self.phone or f"guest-{self.pk}" @property def is_authenticated(self): # این مدل AUTH_USER_MODEL نیست؛ این property فقط برای سازگاری با # rest_framework.permissions.IsAuthenticated لازمه. return True class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') birth_year = models.PositiveSmallIntegerField(null=True, blank=True) parent_phone = models.CharField(max_length=15, blank=True) bio = models.CharField(max_length=200, blank=True) def __str__(self): return f"profile-{self.user_id}" class Session(models.Model): """ نگهداری refresh_token هر ورود. توکن دسترسی (access token) در دیتابیس ذخیره نمی‌شه؛ به‌صورت stateless امضا (signed) می‌شه — دقیقاً طبق فیلدهای همین مدل که فقط refresh_token داره، نه access token. """ user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sessions') refresh_token = models.CharField(max_length=200, unique=True) device_id = models.CharField(max_length=64, blank=True) expires_at = models.DateTimeField() revoked = models.BooleanField(default=False) def __str__(self): return f"session-{self.pk}-user{self.user_id}" class OTPCode(models.Model): phone = models.CharField(max_length=15, db_index=True) code = models.CharField(max_length=6) expires_at = models.DateTimeField() is_used = models.BooleanField(default=False) def __str__(self): return f"otp-{self.phone}"