Harden auth, AI tutor, payments, and subscriptions

This commit is contained in:
2026-08-22 04:42:36 +03:30
parent f945f960c9
commit cb18c93a3e
41 changed files with 2202 additions and 595 deletions

View File

@@ -10,29 +10,12 @@ from django.utils import timezone
from statuslog.signals import status_changed
from .models import Node, NodeProgress, UserLessonCompletion
from .models import NodeProgress, UserLessonCompletion
def _get_or_create_progress(user_id, node):
"""
⚛️🔒 اصلاح race condition: قبلاً این تابع فقط get_or_create می‌کرد و
ردیفِ برگردانده‌شده (چه تازه‌ساخته‌شده، چه از قبل موجود) هیچ‌وقت قفل
نمی‌شد. دو درخواستِ هم‌زمان برای همون (user_id, node) — مثلاً دو پاسخِ
پشت‌سرهم یا یک پاسخ و یک تکمیلِ درس که تقریباً هم‌زمان می‌رسن —
می‌تونستن هر دو همون ردیفِ NodeProgress رو با status/started_at/
last_activity_at یکسان بخونن، هر دو مستقل مقدارهای جدید رو محاسبه
کنن و save کنن؛ نتیجه‌ی یکی از دو نویسش گم می‌شد (lost update) —
مثلاً یک started_at یا یک تغییرِ status می‌تونست توسط نویسش دیگری
که دیرتر commit می‌شه، بازنویسی/نادیده گرفته بشه.
حالا دقیقاً مثل الگوی grant_free_subscription در subscription/services.py:
اول get_or_create برای اطمینان از وجودِ ردیف (idempotent، خودش با
savepoint در برابر race روی خودِ create محافظت‌شده)، بعد همون ردیف با
select_for_update دوباره خونده و قفل می‌شه تا خواندن/تغییر/ذخیره‌ی
فیلدهاش توسط caller، در برابر concurrent update ایمن باشه. caller
باید این تابع رو همیشه داخل transaction.atomic صدا بزنه (هر دو نقطه‌ی
فراخوانی در همین فایل همین‌طورن).
"""
"""get_or_create ایجاد می‌کند، سپس ردیف با select_for_update دوباره خوانده و قفل می‌شود
تا خواندن/تغییر/ذخیره در برابر concurrent update ایمن باشد. باید داخل transaction.atomic صدا زده شود."""
NodeProgress.objects.get_or_create(user_id=user_id, node=node)
progress = NodeProgress.objects.select_for_update().get(user_id=user_id, node=node)
return progress, False