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

@@ -29,29 +29,9 @@ class Command(BaseCommand):
)
count = 0
# ⚛️ atomic (به‌ازای هر رکورد، نه کل حلقه): این یک job دوره‌ای رو
# روی هزاران رکورد می‌تونه اجرا کنه. اگه کل حلقه یک atomic واحد
# باشه، یک خطا روی رکورد هزارم، ابطالِ ۹۹۹ رکورد درست‌شده‌ی قبلی رو
# هم rollback می‌کنه (و کل job باید از اول اجرا بشه). به‌جاش هر
# رکورد (save + signal که StatusChangeLog می‌سازه) جدا اتمیکه: یک
# خطای مقطعی فقط همون رکورد رو نصفه‌کاره نگه نمی‌داره، رکوردهای
# قبلی/بعدی مستقل می‌مونن.
#
# ⚛️🔒 اصلاح race condition (lost update): قبلاً همین آبجکتِ
# progressِ خونده‌شده از stale_qs (بدونِ قفل) مستقیماً save می‌شد.
# اگه هم‌زمان یک درخواستِ کاربرِ واقعی (touch_node_progress_on_answer
# یا update_node_progress_on_lesson_completion در content/services.py،
# هر دو با select_for_update) دقیقاً همون ردیف رو به‌روزرسانی و
# last_activity_at رو تازه می‌کرد، این کامند می‌تونست بدونِ اطلاع از
# اون تغییرِ تازه، status رو با مقدارِ قدیمیِ در-حافظه به 'abandoned'
# برگردونه — یعنی فعالیتِ تازه‌ی کاربر توسط این جاب rollback/overwrite
# می‌شد. حالا برای هر رکورد، فقط id از stale_qs گرفته می‌شه؛ ردیفِ
# واقعی داخل transaction.atomic با select_for_update دوباره خونده و
# قفل می‌شه (دقیقاً همون الگویی که مسیرِ کاربر استفاده می‌کنه)، و
# شرطِ نصفه‌رها بودن (status='in_progress' و last_activity_at قدیمی‌تر
# از threshold) بعد از گرفتنِ قفل دوباره چک می‌شه؛ اگه بین queryset
# اولیه و گرفتنِ قفل، یک فعالیتِ کاربر همین ردیف رو تازه کرده باشه،
# دیگه شرط برقرار نیست و این ردیف نادیده گرفته می‌شه (نه abandoned).
# هر رکورد جدا اتمیک است (نه کل حلقه) تا خطای یک رکورد بقیه را rollback نکند.
# id ها جدا گرفته می‌شوند و هر ردیف زیر select_for_update دوباره خوانده و
# شرط abandoned بعد از قفل دوباره چک می‌شود، تا فعالیت هم‌زمان کاربر overwrite نشود.
stale_ids = list(stale_qs.values_list('id', flat=True))
for progress_id in stale_ids:
with transaction.atomic():