The API moved to mathapi.tadrisino2.ir on 2026-09-26 (nginx server_name changed at 23:22) and api.math.tadrisino2.ir no longer resolves. The code default for PUBLIC_BASE_URL and the docs' curl examples still named the dead host, so every paymentUrl, the Zarinpal callback and the Bazaar redirect the backend builds pointed nowhere. Historical status notes in CLAUDE.md keep the old name on purpose. config/tests.py now reads the API host from PUBLIC_BASE_URL instead of a literal. 624 tests green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
147 lines
10 KiB
Markdown
147 lines
10 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` دارد، پس
|
||
برای تست لازم نیست پیامک واقعی برسد.
|
||
|
||
**برای تستِ مسیرِ کامل روی سرورِ واقعی، شمارهی تست بگیرید.** روی
|
||
`mathapi.tadrisino2.ir` سرویسِ پیامک هنوز تنظیم نشده، پس `requestOtp` برای یک
|
||
شمارهی معمولی خطای ۵۰۳ میدهد. بکاند چند شمارهی تست با کدِ ثابت دارد
|
||
(`TEST_LOGIN_PHONES` در `.env` سرور): برای آن شمارهها پیامکی ارسال نمیشود و
|
||
همان کد همیشه جواب میدهد، دقیقاً مثل مسیرِ واقعی. یک جفت «شماره + کد» برای
|
||
خودتان بخواهید — ساختنش کارِ یک خط ویرایشِ `.env` و یک ریاستارت است.
|
||
|
||
> کدِ شمارهی تست در پاسخِ API برنمیگردد. اگر `devOtp` خالی آمد، یعنی همین
|
||
> مسیر فعال است، نه اینکه چیزی خراب باشد.
|
||
|
||
**یک حسابِ تستِ آماده هم روی پروداکشن هست** که از قبل داده دارد، تا برای دیدنِ
|
||
صفحههای اعلان و لیدربرد مجبور نباشید منتظرِ تولیدِ دادهی واقعی بمانید:
|
||
|
||
```bash
|
||
curl -X POST https://mathapi.tadrisino2.ir/api/auth/session \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"type":"guest","deviceId":"frontend-test-device"}'
|
||
```
|
||
|
||
- کاربرِ شمارهی ۶، با نامِ «حساب تست فرانت». هیچ پیامکی و هیچ توکنی که دست
|
||
به دست شود در کار نیست — همان `deviceId` کافیست.
|
||
- `GET /api/notifications` روی این حساب چند اعلانِ واقعی دارد. **پاسخ در کلیدِ
|
||
`items` است**، نه `results`.
|
||
- `GET /api/leaderboard?period=weekly` و `?period=all_time` هر کدام ۶ ردیف
|
||
میدهند، با **ترتیبِ عمداً متفاوت** بینِ دو بازه، تا بشود دید پارامترِ
|
||
`period` واقعاً اثر میکند.
|
||
- این داده دستی روی دیتابیسِ پروداکشن ساخته شده و در گیت نیست؛ پیش از باز
|
||
شدنِ سرویس روی کاربرانِ واقعی باید پاک شود (`docs/operations.md`).
|
||
|
||
⚠️ این حساب **مشترک** است. هر کسی که همین `deviceId` را بفرستد وارد همان حساب
|
||
میشود، پس رویش چیزی که مالِ خودتان باشد نگذارید. برای کارِ روزمره یک
|
||
`deviceId` مخصوصِ خودتان بسازید — هر رشتهای قبول است.
|
||
|
||
**۲. فهرستِ پایهها از `api.grades()` میآید** (انجام شده). ترتیب همان ترتیبِ
|
||
کلاسهاست و `id` هر پایه برابرِ شمارهاش است: `gradeId: 8` یعنی پایهی هشتم.
|
||
فیلدِ `number` هم همین عدد را صریح میدهد. پیش از ۲۰۲۶-۰۹-۲۳ idها ترتیبِ
|
||
واردشدنِ کتابها بودند (هشتم=۵)؛ اپ چون همیشه `gradeId` را از سرور میگیرد،
|
||
تغییری لازم نداشت.
|
||
|
||
**۳. صفحهی درس، ویدیو دارد.** `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/`
|