- /api/ai/ask: at most AI_MAX_CONCURRENT_PER_WORKER (12 of 32 threads) answers per worker; beyond that 503 assistant_busy + Retry-After before any quota is reserved. Without it a burst of questions (3-40 s each) took every thread and the map and quizzes queued behind. The slot is held until the stream is actually finished or closed. - Answer submission 21 -> 14 queries, 15.4 -> 11.0 ms: one query for all choices, locked get_or_create for node progress and streak, and today's streak recorded once per day via the cache instead of a transaction per answer. - /api/home 12.4 -> 8.6 ms: the user's completions are read by user_id instead of an IN list of ~600 lesson ids; the dict is returned directly (serializer stays the Swagger contract, HomeMatchesItsSerializerTests keeps them equal). - otp_login per-IP 30 -> 300/hour: a class of 30 on one school Wi-Fi filled it; guessing is bounded per code and per phone. - /api/health aiConcurrency. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
3.2 KiB
Python
77 lines
3.2 KiB
Python
"""
|
||
استریک: روزهای پیاپیِ فعالیت.
|
||
|
||
تا ۲۰۲۶-۰۹-۲۴ هیچجای کد StreakRecord یا ActiveDay نمینوشت — فقط
|
||
seed_test_data. یعنی «روز پیاپی» روی صفحهی خانه برای هر کاربرِ واقعی برای
|
||
همیشه صفر بود و فقط کاربرهای دمو عدد داشتند.
|
||
|
||
«فعالیت» یعنی پاسخ به سوال یا تمام کردنِ درس/جزیره. هر روز (به وقتِ تهران،
|
||
TIME_ZONE) یک بار شمرده میشود؛ صدا زدنِ مکرر در یک روز بیاثر است.
|
||
"""
|
||
from datetime import timedelta
|
||
|
||
from django.db import transaction
|
||
from django.utils import timezone
|
||
|
||
from .models import ActiveDay, StreakRecord
|
||
|
||
|
||
def record_activity(user_id, today=None):
|
||
"""امروز را روزِ فعال ثبت میکند و استریک را جلو میبرد. idempotent در یک روز."""
|
||
today = today or timezone.localdate()
|
||
with transaction.atomic():
|
||
streak, _ = StreakRecord.objects.select_for_update().get_or_create(user_id=user_id)
|
||
|
||
last = (
|
||
ActiveDay.objects.filter(streak=streak)
|
||
.order_by('-date').values_list('date', flat=True).first()
|
||
)
|
||
if last == today:
|
||
return streak
|
||
|
||
if last == today - timedelta(days=1):
|
||
streak.current_streak += 1
|
||
else:
|
||
streak.current_streak = 1
|
||
streak.longest_streak = max(streak.longest_streak, streak.current_streak)
|
||
streak.save(update_fields=['current_streak', 'longest_streak'])
|
||
ActiveDay.objects.get_or_create(streak=streak, date=today)
|
||
return streak
|
||
|
||
|
||
def effective_current_streak(streak, today=None):
|
||
"""
|
||
استریکی که الان باید نشان داده شود.
|
||
|
||
current_streak فقط در لحظهی فعالیت بهروز میشود؛ کاربری که دو روز
|
||
نیامده هنوز عددِ قبلی را در دیتابیس دارد. اگر آخرین روزِ فعال نه امروز
|
||
است نه دیروز، زنجیره شکسته و عددِ واقعی صفر است.
|
||
"""
|
||
if streak is None:
|
||
return 0
|
||
today = today or timezone.localdate()
|
||
last = (
|
||
streak.active_days.order_by('-date').values_list('date', flat=True).first()
|
||
)
|
||
if last is None or last < today - timedelta(days=1):
|
||
return 0
|
||
return streak.current_streak
|
||
|
||
|
||
def streak_state(streak, today=None):
|
||
"""
|
||
وضعیتِ زنجیره برای نمایش: (current, active_today, lost).
|
||
|
||
lost طولِ زنجیرهای است که شکسته (آخرین روزِ فعال پیش از دیروز)؛ current_streakِ دیتابیس تا فعالیتِ بعدی
|
||
همان عددِ قبلی را نگه میدارد (record_activity آن را به ۱ برمیگرداند)، پس همین عدد است.
|
||
"""
|
||
if streak is None:
|
||
return 0, False, 0
|
||
today = today or timezone.localdate()
|
||
last = streak.active_days.order_by('-date').values_list('date', flat=True).first()
|
||
if last is None:
|
||
return 0, False, 0
|
||
if last < today - timedelta(days=1):
|
||
return 0, False, streak.current_streak
|
||
return streak.current_streak, last == today, 0
|