Harden auth, AI tutor, payments, and subscriptions
This commit is contained in:
@@ -57,19 +57,9 @@ class Question(models.Model):
|
||||
|
||||
|
||||
class Choice(models.Model):
|
||||
"""
|
||||
⚠️ اصلاح نسبت به سند اولیه: دیاگرام معماری فقط فیلد `value` رو برای
|
||||
Choice مشخص کرده بود و فیلد تشخیص گزینهی درست (is_correct) رو نداشت
|
||||
(این نکته توی سند models هم صراحتاً بهعنوان یک ایراد/نقص علامتگذاری
|
||||
شده بود: "یا این فیلد سهواً جا افتاده یا تشخیص جای دیگهای انجام میشه").
|
||||
چون بدون این فیلد امکان پیادهسازی POST /api/questions/{id}/answer
|
||||
(که باید correct را برگردونه) وجود نداره، این فیلد اینجا اضافه شده.
|
||||
اگه منطق تشخیص پاسخ درست قراره جای دیگهای (سرویس content-authoring)
|
||||
انجام بشه، این فیلد رو میشه بعداً حذف/جابهجا کرد.
|
||||
"""
|
||||
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name='choices')
|
||||
value = models.CharField(max_length=100)
|
||||
is_correct = models.BooleanField(default=False) # <-- اصلاح/افزوده شده
|
||||
is_correct = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
@@ -82,15 +72,9 @@ class UserLessonCompletion(models.Model):
|
||||
stars = models.PositiveSmallIntegerField()
|
||||
xp_earned = models.PositiveIntegerField(default=0)
|
||||
coins_earned = models.PositiveIntegerField(default=0)
|
||||
# ⚠️ افزوده شده برای رفع race condition: طبق طراحیِ فعلی، کاربر مجاز به
|
||||
# تکرارِ یک درس هست (فقط بارِ اول xp/coins واقعی میگیره، نگاه کنید به
|
||||
# is_first_completion در LessonCompleteView)، پس یک UniqueConstraint
|
||||
# سختِ (user_id, lesson) درست نیست — جلوی retryِ مشروع رو هم میگیره.
|
||||
# مشکلِ واقعی، دابلکلیک/رتریِ شبکهایِ کلاینت روی *همون* submitِ
|
||||
# تکمیل درسه که میتونست دو ردیف برای یک اتفاق بسازه. راهحل، همون
|
||||
# الگوی Idempotency-Key هست که پروژه از قبل در payment/services.py
|
||||
# برای create_order داره: کلاینت (اختیاری) یک کلید یکتا برای هر submit
|
||||
# واقعی میفرسته و رتریها با همون کلید idempotent میشن.
|
||||
# تکرار درس مجاز است (فقط بار اول xp/coins واقعی میگیرد)، پس قید یکتایی
|
||||
# سخت روی (user_id, lesson) درست نیست. idempotency_key فقط جلوی رکورد
|
||||
# تکراریِ ناشی از دابلکلیک/رتری روی همان submit را میگیرد.
|
||||
idempotency_key = models.CharField(max_length=100, null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@@ -177,26 +161,13 @@ class UserAnswer(models.Model):
|
||||
choice = models.ForeignKey(Choice, on_delete=models.CASCADE, related_name='answers')
|
||||
correct = models.BooleanField()
|
||||
xp_earned = models.PositiveIntegerField(default=0)
|
||||
# ⚠️ افزوده شده برای رفع idempotency در POST /api/questions/{id}/answer:
|
||||
# قبلاً هیچ مکانیزمی برای تشخیصِ رتریِ شبکهای/کلاینتِ همون submit وجود
|
||||
# نداشت — یک retry دقیقاً همون درخواست رو دوباره میفرستاد و یک ردیفِ
|
||||
# UserAnswer تکراری میساخت و (در حالت پاسخ غلط) هارت رو دوبار کم
|
||||
# میکرد. دقیقاً همون الگویی که UserLessonCompletion.idempotency_key
|
||||
# برای LessonCompleteView داره: کلاینت (اختیاری) یک کلید یکتا برای هر
|
||||
# submit واقعی میفرسته و رتریها با همون کلید idempotent میشن؛ بدونِ
|
||||
# کلید (مقدار None)، پاسخهای مشروعِ تکراری (کاربر واقعاً چند بار به یک
|
||||
# سوال پاسخ میده) همچنان مجازن.
|
||||
# کلید اختیاری برای idempotency: رتری روی همان submit رکورد تکراری/کسر دوبارهی هارت نمیسازد.
|
||||
idempotency_key = models.CharField(max_length=100, null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
constraints = [
|
||||
# 🔒 قید یکتاییِ سطح DB: بهاندازهی کافی محدود (user_id +
|
||||
# question + idempotency_key) تا کاربر و سوال رو از هم تفکیک
|
||||
# کنه — یعنی همون کلید توسط کاربرِ دیگه یا برای سوالِ دیگه هیچ
|
||||
# تداخلی نداره. condition تضمین میکنه این قید فقط وقتی
|
||||
# idempotency_key واقعاً پر شده اعمال بشه، نه وقتی خالی/ناله
|
||||
# (وگرنه پاسخهای بدونِ کلید هم به اشتباه یکتا میشدن).
|
||||
# قید فقط وقتی idempotency_key پر باشد اعمال میشود، تا پاسخهای بدون کلید یکتا نشوند.
|
||||
models.UniqueConstraint(
|
||||
fields=['user_id', 'question', 'idempotency_key'],
|
||||
condition=models.Q(idempotency_key__isnull=False) & ~models.Q(idempotency_key=''),
|
||||
|
||||
Reference in New Issue
Block a user