سینیور خواست زودتر به فرانتاند وصل شویم تا از آنجا هم تست شود. اینها
سمتِ ما لازم بودند:
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>
112 lines
7.3 KiB
Markdown
112 lines
7.3 KiB
Markdown
# راهنمای اتصالِ فرانتاند به این بکاند
|
||
|
||
این پوشه برای تیمِ فرانت (`MathApp_frontend`) است. `api.ts` یک کلاینتِ
|
||
تایپدار است که از روی OpenAPI واقعیِ همین سرور نوشته شده — نامِ فیلدها حدس
|
||
نیست.
|
||
|
||
```bash
|
||
cp docs/frontend/api.ts <پروژهی-فرانت>/src/lib/api.ts
|
||
```
|
||
|
||
```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
|
||
تفکیکش کنید («بر اساسِ کتابِ درسی» در برابر «پاسخِ عمومی»).
|
||
|
||
## اجرای محلی
|
||
|
||
```bash
|
||
# بکاند
|
||
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/`
|