Grade ids were the order the books were imported in (ninth=1, eighth=5), so gradeId: 5 meant eighth grade and the admin, the question filter and the API all listed grades out of order. Grade.number (1-12) is now read from the title, new grades take it as their id, and migration 0008 moves existing ids plus every reference to them: Unit, the loose grade_id in accounts/ai_tutor/rag, and the grade target inside campaign JSON. Unnumbered demo grades move above 100 and the id sequence is raised past 12 so they never collide with a real grade. The frontend needs no change: it always reads gradeId from the server. 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` دارد، پس
|
||
برای تست لازم نیست پیامک واقعی برسد.
|
||
|
||
**برای تستِ مسیرِ کامل روی سرورِ واقعی، شمارهی تست بگیرید.** روی
|
||
`api.math.tadrisino2.ir` سرویسِ پیامک هنوز تنظیم نشده، پس `requestOtp` برای یک
|
||
شمارهی معمولی خطای ۵۰۳ میدهد. بکاند چند شمارهی تست با کدِ ثابت دارد
|
||
(`TEST_LOGIN_PHONES` در `.env` سرور): برای آن شمارهها پیامکی ارسال نمیشود و
|
||
همان کد همیشه جواب میدهد، دقیقاً مثل مسیرِ واقعی. یک جفت «شماره + کد» برای
|
||
خودتان بخواهید — ساختنش کارِ یک خط ویرایشِ `.env` و یک ریاستارت است.
|
||
|
||
> کدِ شمارهی تست در پاسخِ API برنمیگردد. اگر `devOtp` خالی آمد، یعنی همین
|
||
> مسیر فعال است، نه اینکه چیزی خراب باشد.
|
||
|
||
**یک حسابِ تستِ آماده هم روی پروداکشن هست** که از قبل داده دارد، تا برای دیدنِ
|
||
صفحههای اعلان و لیدربرد مجبور نباشید منتظرِ تولیدِ دادهی واقعی بمانید:
|
||
|
||
```bash
|
||
curl -X POST https://api.math.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/`
|