Files
MathApp/docs/frontend/README.md
Zareian 0c6b08c9ff make every grade's id its class number and sort grades by it everywhere
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>
2026-09-23 22:47:02 +03:30

147 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# راهنمای اتصالِ فرانت‌اند به این بک‌اند
این پوشه برای تیمِ فرانت (`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/`