سینیور خواست زودتر به فرانتاند وصل شویم تا از آنجا هم تست شود. اینها
سمتِ ما لازم بودند:
CORS با allowlist صریح (نه ALLOW_ALL): احرازِ هویتِ ما توکنِ Bearer در هدر
است، پس origin ناشناس نباید از مرورگرِ کاربر به API ما بزند.
CORS_ALLOW_CREDENTIALS عمداً خاموش میماند — کوکیای در کار نیست و روشن
کردنش فقط سطحِ حمله اضافه میکند. CorsMiddleware بالاتر از CommonMiddleware
گذاشته شد وگرنه ریدایرکتِ APPEND_SLASH و خطای ۴۰۰ هدرِ CORS نمیگیرند و در
مرورگر بهشکلِ خطای مبهمِ شبکه دیده میشوند.
PATCH /api/user/profile اضافه شد؛ تا پیش از این هیچ راهی برای ثبتِ نامِ
کاربر نبود. GET /api/home بدونِ پارامتر حالا پایهی ذخیرهشدهی کاربر را
برمیگرداند و POST /api/ai/ask فقط با lessonId کار میکند — هر دو برای
اینکه فرانت لازم نباشد پایه را خودش نگه دارد. questionCount به
lessons/{id}/intro اضافه شد.
docs/frontend/api.ts و README.md کلاینتِ تایپدار و راهنمای اتصالاند، از
روی OpenAPI واقعیِ همین سرویس تولید شدهاند.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
راهنمای اتصالِ فرانتاند به این بکاند
این پوشه برای تیمِ فرانت (MathApp_frontend) است. api.ts یک کلاینتِ
تایپدار است که از روی OpenAPI واقعیِ همین سرور نوشته شده — نامِ فیلدها حدس
نیست.
cp docs/frontend/api.ts <پروژهی-فرانت>/src/lib/api.ts
import { createClient } from './lib/api';
export const api = createClient({ baseUrl: import.meta.env.VITE_API_URL ?? '/api' });
نگاشتِ صفحاتِ فعلیِ فرانت به API
فرانت الان (src/app/App.tsx) خروجیِ Figma Make است و هیچ فراخوانیِ APIای
ندارد؛ همهی دادهها هاردکد است. این جدول میگوید هر صفحه به چه چیزی وصل شود.
| صفحهی فرانت | فراخوانی | نکته |
|---|---|---|
SplashScreen |
api.auth.isSignedIn() |
«قبلاً حساب دارم» → صفحهی ورود |
| — (وجود ندارد، باید ساخته شود) | auth.requestOtp → auth.login |
پایین ببینید |
OnboardingScreen |
— | ایستا |
ProfileSetupScreen |
profile.update({name}) + profile.setGrade(id) |
GRADES هاردکد را با api.grades() عوض کنید |
HomeScreen |
api.home() + api.summary() |
بدونِ آرگومان، پایهی خودِ کاربر ملاک است |
SkillMapScreen |
api.home() → units[].nodes[] |
status = locked / unlocked / in_progress / completed |
LessonScreen |
lesson.intro(id) + lesson.questions(id) |
پایین ببینید |
QuestionScreen |
lesson.answer(qid, optId) → lesson.complete(id) |
mascotMood حالتِ رُبو را میدهد |
AskAIScreen |
ai.askStream({userQuestion, lessonId}, onDelta) |
فقط lessonId کافیست |
SubscriptionScreen |
subscription.plans() + subscription.purchase(planId) |
قیمتهای هاردکد را بردارید |
چهار چیزی که الان با فرانت جور نیست
۱. صفحهی ورود وجود ندارد. فرانت فقط اسم و پایه میپرسد، ولی احرازِ هویتِ ما OTP با شمارهی موبایل است. دو راه:
- مهمان (سریعترین):
api.auth.guest(deviceId)— بدونِ شماره، کاربر بلافاصله وارد میشود و بعداً میتواند شمارهاش را وصل کند. - کامل: یک صفحهی «شماره + کد» بین Splash و ProfileSetup.
در حالتِ توسعه (DJANGO_DEBUG=1) پاسخِ requestOtp فیلدِ devOtp دارد، پس
برای تست لازم نیست پیامک واقعی برسد.
۲. فهرستِ پایهها تا ششم است. GRADES در App.tsx هاردکدِ ۱ تا ۶ است، ولی
جهتِ محصول از ششم به بالا رفته (ترانزیشنی: ۶، ۷، ۹، ۱۰). از api.grades()
بخوانید تا هر پایهای که اضافه شد خودبهخود بیاید.
۳. صفحهی درس، ویدیو دارد. LessonScreen فهرستی از
{type: "video" | "quiz"} نشان میدهد. بکاند هنوز مفهومِ ویدیو ندارد —
lessons/{id}/intro فقط متن و حالتِ ماسکوت میدهد. فعلاً فقط quiz را نشان
دهید؛ اگر ویدیو واقعاً لازم است بگویید تا در مدل اضافه کنیم.
۴. «سطح» در آمار. HomeScreen سه عدد نشان میدهد: روز پیاپی، امتیاز، سطح.
دوتای اول را داریم (streak.current، xp)؛ «سطح» را نداریم. یا از روی xp
سمتِ فرانت حساب کنید، یا بگویید تا در /api/user/summary اضافه کنیم.
چیزهایی که سمتِ بکاند برایتان درست شد
- CORS اضافه شد. origin خود را در
CORS_ALLOWED_ORIGINSبدهید (در حالتِ توسعه پورتهای ۳۰۰۰ و ۵۱۷۳ پیشفرض بازند). PATCH /api/user/profileاضافه شد — قبلاً پروفایل فقط خواندنی بود و هیچ راهی برای ثبتِ اسم نبود.GET /api/homeبدونِ پارامتر حالا پایهی ذخیرهشدهی کاربر را میخواند. قبلاً اگر?grade=نمیفرستادید نقشه خالی برمیگشت.POST /api/ai/askبا فقطlessonIdحالا خودش میفهمد آن درس مالِ کدام پایه و فصلِ کتاب است. قبلاً بایدsubject+grade+lessonNumberرا خودتان میفرستادید وگرنه پاسخ بدونِ استناد به کتاب تولید میشد.
چند قاعده که وقتتان را میخرد
Idempotency-Key بفرستید روی هر چیزی که پول/امتیاز/قلب جابهجا میکند
(answer, complete, purchase). یک UUID که تا موفق شدنِ درخواست ثابت
بماند. بدونِ آن، قطعیِ شبکه + دکمهی دوباره = دوبار کم شدنِ قلب.
۴۰۱ یعنی توکن منقضی شده، نه اینکه کاربر بیرون است. کلاینت خودش یکبار
refresh میزند و درخواست را تکرار میکند؛ فقط اگر آن هم شکست خورد کاربر را
به صفحهی ورود ببرید.
۴۲۹ و ۵۰۳ را جدا نشان دهید. ۴۲۹ یعنی سقفِ روزانهی سوال از دستیار پر شده
(الان ۱۵ تا) یا throttle؛ پیامش «فردا برگرد» است. ۵۰۳ یعنی سرویسِ AI موقتاً
قطع است؛ پیامش «چند لحظه دیگر دوباره». ApiError.isRateLimited و
.isProviderDown همین را میدهند.
پرداخت را در مرورگر باز کنید. purchase یک paymentUrl میدهد که به
سرورِ خودمان اشاره میکند، نه به زرینپال. اپ نباید مستقیم با درگاه حرف بزند.
امتیاز موقعِ اتمامِ درس در مجموع مینشیند، نه سرِ هر سوال. پاسخِ
answer فیلدِ xpEarned دارد ولی آن ارزشِ همان سوال است؛ user/summary
تا وقتی complete صدا نخورد تکان نمیخورد. در UI میتوانید خوشبینانه
جمع بزنید، ولی عددِ معتبر بعد از complete میآید.
complete دو فیلدِ اجباری دارد: correctCount و totalQuestions.
سرور correctCount را با تعدادِ واقعیِ سوالهای آن درس سقف میزند، پس
عددِ متورم امتیاز نمیسازد.
usedChunks خالی یعنی پاسخ به کتاب مستند نیست. اگر خواستید، در UI
تفکیکش کنید («بر اساسِ کتابِ درسی» در برابر «پاسخِ عمومی»).
اجرای محلی
# بکاند
DJANGO_DEBUG=1 python manage.py runserver # 127.0.0.1:8000
# فرانت
VITE_API_URL=http://127.0.0.1:8000/api npm run dev
مستنداتِ زندهی همهی endpointها: http://127.0.0.1:8000/api/docs/