Docs brought to the current state: CLAUDE.md rewritten, README formal, handbook and diagrams redrawn

- CLAUDE.md: 1,225 → ~430 lines, current state only (the dated status log stays in git history).
  Server rules moved to the top: what is ours on 249/247 and what is not, non-negotiable rules,
  announce-before-acting, password handling, and the full "deploy it" procedure with verification.
- README: formal entry page with current numbers (10 grades, 18,522 questions, 61 API operations,
  973 tests, PostgreSQL/Redis).
- Removed docs/frontend/ (Figma-era guide with guest login and its own api.ts; the frontend repo has
  the real client). Swagger's external-docs link now points at the frontend repo.
- architecture/operations/security/content-authoring/loadtest/textbooks: SQLite, guest login,
  demo accounts, question packs and the port-80 note replaced by what is true now.
- Technical handbook text updated throughout; all seven diagrams redrawn (PostgreSQL, phone-only
  login, subscriptions, current data model); PDF rebuilt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 18:45:51 +03:30
parent eb95209ea8
commit a4e0c576bb
20 changed files with 752 additions and 2134 deletions

View File

@@ -4,10 +4,10 @@
| | |
|---|---|
| تاریخ | ۱۰ مهر ۱۴۰۵ (۲ اکتبر ۲۰۲۶) |
| تاریخ | ۲۲ مهر ۱۴۰۵ (۱۴ اکتبر ۲۰۲۶) |
| نویسنده | علیرضا زارعیان — بک‌اند و فرانت‌اند |
| مخاطب | توسعه‌دهنده‌ی تازه‌وارد، مدیرِ فنی، و هر کسی که باید درباره‌ی ادامه‌ی پروژه تصمیم بگیرد |
| کدِ مبنا | بک‌اند `8a4753b` (مخزنِ `Tadrisino/MathApp` ) · فرانت `33f5f8d` (مخزنِ `Tadrisino/MathApp_frontend` ) |
| کدِ مبنا | شاخه‌ی `main` ِ هر دو مخزن در همین تاریخ: `Tadrisino/MathApp` (بک‌اند) و `Tadrisino/MathApp_frontend` (فرانت و پوسته‌ی اندروید) |
| نشانی‌ها | اپ: `math.tadrisino2.ir` · API: `mathapi.tadrisino2.ir` · مستنداتِ زنده‌ی API: `/api/docs/` |
> این فایل نسخه‌ی Markdownِ «مستند-فنی-تدریسینو» است. نسخه‌ی PDF با همان محتوا و صفحه‌آراییِ کامل کنارِ همین فایل است: `technical-handbook.pdf`.
@@ -18,12 +18,13 @@
| شاخص | مقدار |
|---|---|
| سوم، ششم، هفتم، هشتم تا دوازدهم | **۸ پایه** |
| سؤالِ چهارگزینه‌ای از مسئله‌های کتاب | **۲٬۵۴۵** |
| اندپوینتِ API، مستند در Swagger | **۵۹** |
| تستِ خودکار، همه سبز | **۷۷۹** |
| پایه‌های سوم تا دوازدهم | **۱۰ پایه** |
| کتاب (ریاضی، حسابان، هندسه، آمار و احتمال، گسسته) | **۱۵ کتاب در ۵ درس** |
| سؤالِ چهارگزینه‌ای | **۱۸٬۵۲۲** |
| عملیاتِ API، مستند در Swagger | **۶۱** |
| تستِ خودکار، همه سبز | **۹۷۳** |
| اپِ جنگو، هرکدام یک مسئولیت | **۱۴** |
| تکه‌ی کتاب در Qdrant (۱۱ collection) | **۳٬۵۱۷** |
| تکه‌ی کتاب در Qdrant (۱۸ collection) | **۵٬۸۱۸** |
| همراهِ گیمیفیکیشن، هرکدام ۶ حالتِ چهره | **۴** |
| بک‌اند و فرانت؛ یک قراردادِ API | **۲ مخزن** |
@@ -31,7 +32,6 @@
| کاربر | چه می‌کند | کجای کد |
|---|---|---|
| دانش‌آموزِ مهمان | بدونِ شماره وارد می‌شود، پایه‌اش را انتخاب می‌کند و بازی می‌کند. اگر بعداً با شماره ثبت‌نام کند، همان حساب می‌ماند و امتیازش گم نمی‌شود. | `accounts/views.py` |
| دانش‌آموز | از هر سنی (بزرگسالی که فقط یک مبحث را می‌خواهد هم پایه‌ی همان مبحث را انتخاب می‌کند). با شماره‌ی موبایل و کدِ پیامکی وارد می‌شود؛ جزیره‌ها را بازی می‌کند، از دستیار می‌پرسد، امتیاز و سکه و زنجیره‌ی روزانه جمع می‌کند، همراه انتخاب می‌کند، اشتراک می‌خرد، تیکتِ پشتیبانی می‌فرستد. | `content/` `progress/` `ai_tutor/` |
| والد | حسابِ جدا ندارد: «گزارشِ والدین» را در حسابِ خودِ دانش‌آموز می‌بیند — زمانِ تمرین، سوال‌ها و مقایسه با هفته‌ی پیش، با دو نمودار. | `progress/report.py` |
| ادمین (کارمندِ شرکت) | از پنلِ `/admin/` سؤال، درس، پلن، همراه و تیکت را بدونِ برنامه‌نویس ویرایش می‌کند؛ سؤال‌ها را فله‌ای وارد و خارج می‌کند. | `*/admin.py` |
@@ -43,6 +43,7 @@
| مرداد ۱۴۰۵ | طراحیِ معماری و مدل‌های داده؛ هویتِ کاربر؛ جداسازیِ اپ‌ها. بازبینیِ امنیتیِ اول و بستنِ حفره‌ها، محدودیتِ نرخ در کلِ سیستم. |
| شهریور | دستیار از پاسخِ ساختگی به RAGِ واقعی روی کتاب رسید؛ اتصالِ زرین‌پال؛ گیمیفیکیشن؛ خواندنِ پنج کتابِ ریاضی و ساختِ هزاران سؤالِ تأییدشده؛ حسابانِ ۱۱ و ۱۲. |
| ۲ تا ۷ مهر | فرانت به ما سپرده شد و از نو ساخته شد؛ هر دو روی دامنه‌ی تازه زنده شدند؛ پیامکِ واقعی؛ شکل در جوابِ دستیار؛ همراهِ چهارم؛ بازبینیِ نهایی روی سایتِ زنده. |
| ۸ تا ۲۲ مهر | ورودِ مهمان حذف شد؛ کلیدِ امضای تازه؛ PostgreSQL و Redisِ اختصاصی و کپیِ زنده‌ی دیتابیس روی سرورِ دوم؛ دیوارِ پرداخت و اشتراکِ یک/سه/شش‌ماهه از زرین‌پال و کافه‌بازار؛ پایه‌های چهارم و پنجم و پنج کتابِ تازه (هندسه، آمار، گسسته)؛ جزیره‌های تمرین و ۱۸ هزار سؤال؛ سخت‌کردنِ سرور و آزمونِ بارِ هزار نفره. |
### مدلِ سناریو (Use Case)
@@ -50,11 +51,11 @@
![مدل سناریو (Use Case)](images/technical-handbook/use-case-model.png)
*شکلِ ۱-۱ — مدلِ سناریو: سه نقشِ کاربری (والد با حسابِ خودِ دانش‌آموز)، یازده کارِ اصلی، و سه سرویسِ بیرونی.*
*شکلِ ۱-۱ — مدلِ سناریو: نقش‌های کاربری (والد با حسابِ خودِ دانش‌آموز)، کارهای اصلی، و سرویس‌های بیرونی.*
### واحدهای محتوا (این واژه‌ها در کلِ سند تکرار می‌شوند)
پایه Grade ۸ ← فصل Unit ۵۸ ← جزیره Node ۲۶۳ ← درس Lesson ۲٬۶۱۷ ← سؤال Question ۲٬۵۴۵ ← گزینه Choice
پایه Grade ۱۰ ← کتاب Subject ۵ ← فصل Unit ۸۹ ← جزیره Node ۱٬۱۴۵ ← درس Lesson ۵٬۴۰۶ ← سؤال Question ۱۸٬۵۲۲ ← گزینه Choice
**«درس» در این پروژه یعنی یک «تیپ‌مسئله»ی کتاب**، نه یک جلسه‌ی کلاس. برای همین درس خیلی ریز است و اپ در سطحِ **جزیره** (Node) بازی می‌شود: آزمونِ هر جزیره از درس‌هایش سؤال برمی‌دارد.
@@ -72,15 +73,15 @@
| لایه | انتخاب | چرا | چرا نه جایگزین | هزینه / ریسک |
|---|---|---|---|---|
| بک‌اند | **Django 5.2** + **Django REST Framework** | ORM و migration و **پنلِ ادمینِ آماده** در یک بسته؛ پنلِ ادمین یعنی محتوا بدونِ برنامه‌نویس ویرایش می‌شود. DRF احرازِ هویت، مجوز، محدودیتِ نرخ و سریال‌سازی را استاندارد می‌دهد. پایتون همان زبانِ کتابخانه‌های هوشِ مصنوعی است ( `openai`، `qdrant-client`، `pymupdf` ). | FastAPI سریع‌تر و async است ولی ادمین، ORM و migration را باید جدا سرهم کرد. Node.js زبانِ بک‌اند را از زبانِ لایه‌ی AI جدا می‌کرد. | جنگو پیش‌فرض همگام (sync) است؛ جوابِ طولانیِ دستیار یک نخ را نگه می‌دارد. با ۲۴ نخِ هم‌زمان حل شد (بخشِ ۱۴). |
| بک‌اند | **Django 5.2** + **Django REST Framework** | ORM و migration و **پنلِ ادمینِ آماده** در یک بسته؛ پنلِ ادمین یعنی محتوا بدونِ برنامه‌نویس ویرایش می‌شود. DRF احرازِ هویت، مجوز، محدودیتِ نرخ و سریال‌سازی را استاندارد می‌دهد. پایتون همان زبانِ کتابخانه‌های هوشِ مصنوعی است ( `openai`، `qdrant-client`، `pymupdf` ). | FastAPI سریع‌تر و async است ولی ادمین، ORM و migration را باید جدا سرهم کرد. Node.js زبانِ بک‌اند را از زبانِ لایه‌ی AI جدا می‌کرد. | جنگو پیش‌فرض همگام (sync) است؛ جوابِ طولانیِ دستیار یک نخ را نگه می‌دارد. با ۴ worker × ۳۲ نخ و سهمِ جدای دستیار حل شد (بخشِ ۱۴). |
| قراردادِ API | **OpenAPI** با drf-spectacular | اسکیما از خودِ کد ساخته می‌شود، پس هیچ‌وقت از کد عقب نمی‌ماند. Swagger در `/api/docs/` قراردادِ مشترکِ دو مخزن است. | سندِ دستی یا «در ذهنِ کسی» — که دقیقاً باعثِ ناهمخوانیِ اولیه‌ی فرانت و بک‌اند شد. | — |
| فرانت | **React 18** + **TypeScript** (strict) + **Vite** + **Tailwind** | فرانتِ اولیه را توسعه‌دهنده‌ی قبلی با React ساخته بود؛ در بازسازی نگه داشته شد چون بزرگ‌ترین اکوسیستم را دارد و نیروی جایگزینش راحت پیدا می‌شود. TypeScript ناهمخوانیِ شکلِ داده با API را در زمانِ build می‌گیرد. Vite buildِ سریع و Tailwind رنگ و فاصله‌ی یکدست. وبِ موبایل‌محور یعنی بدونِ نصب و بدونِ فروشگاه روی هر گوشی باز می‌شود. | React Native / Flutter نیازِ انتشار در فروشگاه و build جدا داشت؛ نسخه‌ی اندروید (کافه‌بازار) بعداً می‌تواند اضافه شود. | بدونِ کتابخانه‌ی router و state؛ کمتر وابستگی، ولی الگوی داخلی را باید خواند (بخشِ ۴). |
| دیتابیس | **SQLite** (فعلاً) | روی سرورِ مشترکِ شرکت صفر نگهداری دارد: یک فایل، بکاپ با یک دستور. برای حجمِ فعلی کافی است. کد از ORM استفاده می‌کند و به SQLite گره نخورده. | Postgres روی همان سرور موجود است؛ مهاجرت به هماهنگی با ادمینِ سرور نیاز دارد و به مرحله‌ی بعد موکول شده است. | **فقط یک نویسنده در لحظه.** اولین گلوگاهِ مقیاس است (بخشِ ۱۴). |
| فرانت | **React 18** + **TypeScript** (strict) + **Vite** + **Tailwind** | فرانتِ اولیه را توسعه‌دهنده‌ی قبلی با React ساخته بود؛ در بازسازی نگه داشته شد چون بزرگ‌ترین اکوسیستم را دارد و نیروی جایگزینش راحت پیدا می‌شود. TypeScript ناهمخوانیِ شکلِ داده با API را در زمانِ build می‌گیرد. Vite buildِ سریع و Tailwind رنگ و فاصله‌ی یکدست. وبِ موبایل‌محور یعنی بدونِ نصب و بدونِ فروشگاه روی هر گوشی باز می‌شود. | React Native / Flutter نیازِ انتشار در فروشگاه و build جدا داشت. نسخه‌ی اندروید یک پوسته‌ی WebView روی همین سایت است که فقط پرداختِ کافه‌بازار را بومی انجام می‌دهد ( `android/` در مخزنِ فرانت). | بدونِ کتابخانه‌ی router و state؛ کمتر وابستگی، ولی الگوی داخلی را باید خواند (بخشِ ۴). |
| دیتابیس و کش | **PostgreSQL 14** (clusterِ اختصاصی) · **Redis** (اختصاصی) | قفلِ ردیفیِ واقعی برای پرداخت، قلب و سهمیه؛ چند نویسنده‌ی هم‌زمان؛ کپیِ زنده‌ی لحظه‌ای به سرورِ دوم با ابزارِ خودِ Postgres. Redis سقف‌های نرخ و کش را بینِ workerها مشترک می‌کند. توسعه‌ی محلی و تست‌ها روی SQLite می‌مانند. | Postgres و Redisِ موجودِ ماشین مالِ تیم‌های دیگرند؛ برای این‌که به داده‌ی دیگران دست نزنیم، نمونه‌ی جدای خودمان را داریم. | دو سرویسِ بیشتر برای نگهداری؛ اتصال‌ها از poolِ هر worker می‌آیند چون `max_connections` ِ سرور ۶۰ است. |
| جست‌وجوی کتاب | **Qdrant** (بانکِ برداری) | تیمِ دیگرِ شرکت کتاب‌های سوم/ششم/هفتم را از قبل در یک Qdrantِ مشترک گذاشته بود؛ همان را ادامه دادیم تا داده دوباره ساخته نشود. فیلتر روی فیلدِ «فصل» دارد. | pgvector به Postgres نیاز داشت؛ FAISS سرور ندارد و بینِ دو تیم اشتراک‌پذیر نیست. | سرویسِ مشترک با تیمِ دیگر؛ پاک‌کردن باید با احتیاط باشد (بخشِ ۹). |
| مدلِ زبانی | **GapGPT** ← `gpt-4.1-mini` | GapGPT یک واسطِ سازگار با OpenAI است که از داخلِ ایران جواب می‌دهد. مدل با سنجش انتخاب شد، نه با جدولِ قیمت: روی ۱۰۲ مسئله‌ی تصحیح‌شده ۹۵–۹۶ درست در برابرِ ۸۵–۹۰ برای `gpt-4o-mini` ؛ هر جواب با پرامپتِ امروز حدودِ ۰٫۲ سنت (≈ ۶۰۰ تومان؛ `docs/ai-cost-and-guardrails.md`). دستیار فقط درباره‌ی ریاضی حرف می‌زند و سقفِ روزانه‌اش برای کاربرِ رایگان ۳ و برای مشترک به اندازه‌ی پلن است. | OpenAIِ مستقیم از ایران در دسترس نیست. مدل‌های گران‌تر در دورِ اولِ سنجش (۱۴ مدل) مزیتی نشان ندادند. | کُندیِ سرویس (حتی «سلام» ۷ ثانیه) دستِ ما نیست؛ اعتبار دو بار تمام شده. |
| ورود | **توکنِ امضاشده‌ی خودمان** + کدِ پیامکی | کاربرها با شماره وارد می‌شوند، نه نام‌کاربری/رمز؛ مدلِ کاربرِ جنگو به این نمی‌خورد. توکن با `django.core.signing` امضا می‌شود؛ یک وابستگیِ کمتر از کتابخانه‌ی JWT. | JWT همان کار را می‌کند با یک کتابخانه‌ی اضافه. | امنیتش به کلیدِ امضا بسته است (بخشِ ۱۲، موردِ ۱). |
| مدلِ زبانی | **GapGPT** ← `gpt-4.1-mini` | GapGPT یک واسطِ سازگار با OpenAI است که از داخلِ ایران جواب می‌دهد. مدل با سنجش انتخاب شد، نه با جدولِ قیمت: روی ۱۰۲ مسئله‌ی تصحیح‌شده ۹۵–۹۶ درست در برابرِ ۸۵–۹۰ برای `gpt-4o-mini` ؛ هر جواب با پرامپتِ امروز حدودِ ۰٫۲ سنت (≈ ۶۰۰ تومان؛ `docs/ai-cost-and-guardrails.md`). دستیار فقط درباره‌ی ریاضی حرف می‌زند و سقفِ روزانه‌اش برای کاربرِ رایگان ۲ و برای مشترک ۴ است. | OpenAIِ مستقیم از ایران در دسترس نیست. مدل‌های گران‌تر در دورِ اولِ سنجش (۱۴ مدل) مزیتی نشان ندادند. | کُندیِ سرویس (حتی «سلام» ۷ ثانیه) دستِ ما نیست؛ اعتبار دو بار تمام شده. |
| ورود | **توکنِ امضاشده‌ی خودمان** + کدِ پیامکی | کاربرها با شماره وارد می‌شوند، نه نام‌کاربری/رمز؛ مدلِ کاربرِ جنگو به این نمی‌خورد. توکن با `django.core.signing` امضا می‌شود؛ یک وابستگیِ کمتر از کتابخانه‌ی JWT. | JWT همان کار را می‌کند با یک کتابخانه‌ی اضافه. | امنیتش به کلیدِ امضا بسته است؛ کلید روی خودِ سرور ساخته شده و در هیچ مخزنی نیست (بخشِ ۱۲). |
| پیامک / پرداخت | **کاوه‌نگار** · **زرین‌پال** · **کافه‌بازار** | سرویس‌های ایرانیِ رایج؛ کاوه‌نگار با «ارسال با الگو» که روی خطِ شرکت فعال است؛ زرین‌پال برای وب، بازار برای خریدِ درون‌برنامه‌ایِ اندروید. | — | پرداختِ واقعی تا انتها هنوز آزموده نشده. |
| اجرا | **gunicorn** (gthread) پشتِ **nginx** پشتِ **ArvanCloud** | الگوی استانداردِ اجرای جنگو. آروان CDN و سپرِ حمله‌ی ایرانی است. WhiteNoise فایل‌های استاتیکِ ادمین را از خودِ جنگو سرو می‌کند. | — | سرور مشترک است؛ مالِ ما فقط یک پوشه و یک سرویس. |
| اجرا | **gunicorn** (gthread) پشتِ **nginx** پشتِ **ArvanCloud** | الگوی استانداردِ اجرای جنگو. آروان CDN و سپرِ حمله‌ی ایرانی است و nginx فقط درخواستِ رسیده از آروان را می‌پذیرد. WhiteNoise فایل‌های استاتیکِ ادمین را از خودِ جنگو سرو می‌کند. | — | سرور مشترک است؛ مالِ ما فقط پوشه‌ها و سرویس‌های خودمان (بخشِ ۱۳). |
> #### اصلِ کلیِ انتخاب‌ها
>
@@ -96,17 +97,17 @@
### درونِ بک‌اند: «یک برنامه، چند اپِ مستقل» (Modular Monolith)
بک‌اند یک برنامه‌ی جنگوست که به ۱۴ «اپ» تقسیم شده (اپِ `family` ۱۰ مهر ۱۴۰۵ حذف شد). هر اپ یک پوشه است با مدل‌ها، APIها و تست‌های خودش. این همان الگویی است که اجازه می‌دهد چند نفر هم‌زمان روی بک‌اند کار کنند (بخشِ ۱۴).
بک‌اند یک برنامه‌ی جنگوست که به ۱۴ «اپ» تقسیم شده. هر اپ یک پوشه است با مدل‌ها، APIها و تست‌های خودش. این همان الگویی است که اجازه می‌دهد چند نفر هم‌زمان روی بک‌اند کار کنند (بخشِ ۱۴).
| اپ | مسئولیت | مدل‌ها (جدول‌ها) |
|---|---|---|
| `accounts` | هویت، ورود با کدِ پیامکی، نشستِ ورود، پروفایل | `User Profile Session OTPCode` |
| `content` | برنامه‌ی درسی، سؤال، آزمونِ جزیره، ثبتِ جواب و پیشرفت | `Grade Unit Node Lesson Question Choice UserAnswer UserLessonCompletion UserNodeCompletion NodeProgress QuestionReport` |
| `accounts` | هویت، ورود با کدِ پیامکی، نشستِ ورود، پروفایل | `User Profile Session OTPCode LoginEvent` |
| `content` | کتاب‌ها، برنامه‌ی درسی، سؤال، آزمونِ جزیره، ثبتِ جواب و پیشرفت، رأی و گزارشِ دانش‌آموز | `Grade Subject Unit Node Lesson Question Choice UserAnswer UserLessonCompletion UserNodeCompletion NodeProgress ContentVote QuestionReport LessonReport SeedRecord` |
| `progress` | امتیاز، سکه، قلب، زنجیره، جدولِ رتبه، همراه‌ها | `UserStats UserHearts StreakRecord ActiveDay LeaderboardEntry Character` |
| `ai_tutor` | گفتگو با دستیار، سهمیه، بازخوردِ 👍/👎 | `Conversation Message MessageFeedback AskIdempotency` |
| `rag` | خواندنِ کتاب، Qdrant، ساختِ پرامپت، شکل‌ها | `LessonChunk LessonChunkImage IngestedPage` |
| `subscription` | پلن‌های اشتراک و اشتراکِ هر کاربر | `Plan Subscription` |
| `payment` | سفارش، تراکنش، زرین‌پال، بازار، لغو و بازپرداخت از ادمین | `Order Transaction BazaarCredential` |
| `payment` | سفارش، تراکنش، زرین‌پال، بازار، لغو و بازپرداخت از ادمین، دفترِ رویدادِ خرید | `Order Transaction PaymentEvent BazaarCredential` |
| `discount` | کدِ تخفیف و پیشنهادها | `DiscountCode Redemption Offer UserOffer` |
| `affiliate` | معرفی به دوستان و پورسانت | `AffiliateAccount Conversion` |
| `notifications` | اعلان‌های داخلِ اپ | `Notification` |
@@ -148,19 +149,22 @@ MathApp/
├── config/ تنظیمات و سیم‌کشیِ کلِ پروژه
│ ├── settings.py همه‌ی تنظیمات؛ خواندنِ .env؛ نرخِ throttleها؛ کلیدِ امضا؛ مدل‌های AI
│ ├── urls.py ریشه‌ی مسیرها: /admin، /api/docs، /api/health و include هر اپ
│ ├── middleware.py لاگِ JSONِ هر درخواست، no-store، سقفِ ورودِ ادمین
│ ├── middleware.py IPِ واقعی فقط از آروان، لاگِ JSONِ هر درخواست، no-store، سقفِ ورودِ ادمین
│ ├── exceptions.py هر خطا → پاسخِ JSONِ یکدست، بی‌جزئیاتِ داخلی
│ ├── health.py GET /api/health: دیتابیس، migration، کش، محتوا، هشدارها
│ ├── health.py GET /api/health: دیتابیس، migration، کش، pool، بکاپ، هشدارها
│ ├── db.py · cache.py poolِ اتصالِ Postgres (۵۰۳ به‌جای ۵۰۰)، Redisِ تحمل‌پذیر
│ ├── renderers.py خروجیِ JSON بی‌اعراب و بی «ی»ِ اضافه
│ └── log_formatters.py قالبِ JSONِ لاگ
├── accounts/ content/ progress/ ai_tutor/ rag/ subscription/ payment/
├── discount/ affiliate/ notifications/ support/ campaigns/ statuslog/
├── content/seed/ curriculum.json و questions.json — کلِ محتوا به‌صورتِ فایل در git
├── data/textbooks/ PDFِ کتاب‌ها (در git نیست؛ فقط README)
├── templates/tadrisino/ پوسته‌ی راست‌به‌چپِ Swagger و ReDoc
├── tools/fake_openai.py جایگزینِ GapGPT برای تستِ بی‌هزینه
├── tools/ fake_openai.py (جایگزینِ GapGPT) · loadtest/ (آزمونِ بار)
├── ops/ نسخه‌ی اصلیِ nginx، بکاپِ Postgres و آرشیوِ لاگ روی سرورها
├── docs/ operations، architecture، security-before-production، content-authoring، …
├── deploy.sh کلِ انتشار در یک اسکریپت (بخشِ ۱۳)
├── gunicorn.conf.py ۸ نخ در هر worker، timeoutِ ۱۲۰
├── gunicorn.conf.py ۴ worker × ۳۲ نخ، timeoutِ ۱۲۰
├── requirements.txt Django 5.2، DRF، drf-spectacular، openai، qdrant-client، pymupdf، …
├── docker-compose.yml فقط محلی: Loki + Alloy + Grafana برای دیدنِ لاگ
└── manage.py
@@ -198,7 +202,7 @@ src/
├── state/ داده‌ی مشترک (پروفایل، نقشه) و گفتگوی دستیار
├── screens/ یک فایل برای هر صفحه: HomeScreen، QuizScreen، AskScreen، ReportScreen، …
├── components/ اجزای مشترک: مسیرِ جزیره‌ها، برگه‌ی پایین، نوارِ پایین
├── mascots/ سیگما، دلتا، پی، تدی — SVG از کد، ۶ حالتِ چهره
├── mascots/ سیگما، دلتا، پی، امگا — SVG از کد، ۶ حالتِ چهره
├── diagram/ JSONِ شکلِ دستیار → SVG؛ مختصات، ریشه، زاویه این‌جا حساب می‌شود
└── lib/ رقمِ فارسی، تاریخِ شمسی، MathText، پیامِ خطای دوستانه، صدا
```
@@ -230,7 +234,7 @@ Authorization: Bearer 10:1tQx9k:Xy3… ← user_id:زمان:امضا
Idempotency-Key: 7f3c… {"selectedOptionId": 7290}
```
**4. شبکه** — اینترنت ← **ArvanCloud** ← **nginx** روی سرور ← سوکتِ یونیکس ← یک نخِ آزاد از **gunicorn** ( `gunicorn.conf.py` ).
**4. شبکه** — اینترنت ← **ArvanCloud** ← **nginx** روی سرور (فقط درخواستِ رسیده از آروان) ← سوکتِ یونیکس ← یک نخِ آزاد از **gunicorn** ( `gunicorn.conf.py` ).
**5. بک‌اند · middleware** — `config/settings.py → MIDDLEWARE` — لایه‌های عمومی به‌ترتیب: امنیت، **CORS** (آیا درخواست از `math.tadrisino2.ir` آمده؟)، فایل‌های استاتیک، سقفِ ورودِ ادمین، …، `APINoStoreMiddleware` (پاسخ در کشِ آروان نماند)، و در آخر `APIRequestLoggingMiddleware` که در پایان یک خطِ JSON لاگ می‌نویسد ( `config/middleware.py` ).
@@ -241,7 +245,7 @@ path('api/', include('content.urls')), # config/urls.py
path('questions/<int:question_id>/answer', QuestionAnswerView.as_view()), # content/urls.py
```
**7. بک‌اند · هویت** — `accounts/authentication.py` — امضا و انقضای توکن را بی‌رجوع به دیتابیس می‌سنجد و `request.user` را می‌سازد. بی‌هدر یعنی «مهمانِ ناشناس»، نه خطا؛ ویو تصمیم می‌گیرد.
**7. بک‌اند · هویت** — `accounts/authentication.py` — امضا و انقضای توکن را بی‌رجوع به دیتابیس می‌سنجد و `request.user` را می‌سازد. بی‌هدر یعنی «درخواستِ ناشناس»، نه خطا؛ ویو تصمیم می‌گیرد.
```
value = TimestampSigner(salt='tadrisino-access-token').unsign(token, max_age=3600)
@@ -294,7 +298,7 @@ with transaction.atomic():
هر نمودار از راست به چپ خوانده می‌شود: ستون‌ها بازیگرها هستند، پیکان‌ها درخواست‌ها. پیکانِ خط‌چین یعنی پاسخ. شماره‌ها ترتیبِ زمانی‌اند.
### ۶-۱ ورود: مهمان، بعد ثبت‌نام با شماره
### ۶-۱ ورود با شماره و کدِ پیامکی
![سناریوی ورود](images/technical-handbook/scenario-login.png)
@@ -331,17 +335,17 @@ with transaction.atomic():
> - **کافه‌بازار:** اپ با Poolakey اشتراک می‌خرد و سرور توکن را از خودِ بازار می‌سنجد؛ بازار تمدیدِ خودکار را خبر نمی‌دهد، پس سرور نزدیکِ پایانِ دوره خودش می‌پرسد ( `sync_bazaar_renewals` ). کدِ تخفیف با «تخفیف پویا»ی بازار به قیمت می‌رسد.
> - **لغو و بازپرداخت از پنلِ ادمین:** دو action روی سفارش‌ها؛ بازپرداخت پس از صفحه‌ی تأیید روزهای اشتراکِ همان سفارش را پس می‌گیرد. خودِ پول از پنلِ درگاه برگردانده می‌شود ( `docs/operations.md` ).
>
> آزموده‌نشده زرین‌پال درخواستِ پرداخت را با کدِ ۱۰۰ می‌پذیرد، ولی یک پرداختِ واقعی تا انتها هنوز انجام نشده؛ خریدِ بازار منتظرِ ساختِ اپِ اندروید است.
> آزموده‌نشده: زرین‌پال درخواستِ پرداخت را با کدِ ۱۰۰ می‌پذیرد، ولی یک پرداختِ واقعی تا انتها هنوز انجام نشده؛ خریدِ بازار منتظرِ نسخه‌ی تازه‌ی اپِ اندروید است.
### ۶-۵ گزارشِ والدین
والد حسابِ جدا و کدِ دعوت ندارد (تا مهرِ ۱۴۰۵ داشت؛ به تصمیمِ پس از ارائه حذف شد). صفحه‌ی «گزارشِ والدین» در پروفایلِ خودِ دانش‌آموز است.
والد حسابِ جدا ندارد؛ صفحه‌ی «گزارشِ والدین» در پروفایلِ خودِ دانش‌آموز است.
| # | درخواست | چه می‌شود |
|---|---|---|
| ۱ | `GET /api/user/weekly-report` | برای هر یک از ۷ روزِ اخیر و ۷ روزِ قبلش: زمانِ تمرین، سوالِ حل‌شده و درست، امتیاز، درس و جزیره‌ی تمام‌شده، سوال از دستیار. فقط دیتای خودِ صاحبِ توکن. |
| ۲ | — | فرانت جمله‌ی مقایسه («آفرین! این هفته ۲۰ دقیقه بیشتر…») و دو نمودار می‌سازد: زمانِ هر روز در برابرِ همان روزِ هفته‌ی پیش، و سوال‌های درست/نادرستِ هر روز. |
| ۳ | `GET /api/progress/nodes` | پارامترِ `userId` برداشته شد؛ هر کاربر فقط پیشرفتِ خودش را می‌بیند. |
| ۳ | `GET /api/progress/nodes` | هر کاربر فقط پیشرفتِ خودش را می‌بیند؛ شناسه‌ی کاربرِ دیگر پذیرفته نمی‌شود. |
> **تخمینی:** «زمانِ تمرین» اندازه‌گیری نیست: از فاصله‌ی جواب‌ها و پرسش‌ها حساب می‌شود (فاصله‌ی بیش از ۱۰ دقیقه وقفه است). خواندنِ درس بدونِ جواب دادن در آن نیست، پس عدد معمولاً از زمانِ واقعی کمتر است.
@@ -350,25 +354,29 @@ with transaction.atomic():
جدول‌های اصلی، دسته‌بندی‌شده بر اساسِ اپ. `→` یعنی کلیدِ خارجیِ واقعی (داخلِ همان اپ)، و `user_id` یعنی ارجاعِ سبک به اپِ دیگر.
**accounts هویت**
- `User phone, name, is_guest, device_id, grade_id, tokens_valid_after`
- `Profile → User · birth_year, parent_phone`
- `Session → User · refresh_token, expires_at, revoked`
- `User phone, name, grade_id, tokens_valid_after`
- `Profile → User · birth_date (شمسی در API)، birth_year`
- `Session → User · refresh_token (هش‌شده), expires_at, revoked`
- `OTPCode phone, code, expires_at, failed_attempts`
- `LoginEvent ثبتِ هر ورود`
**content محتوا**
- `Grade id = شماره‌ی پایه (۸ = هشتم)`
- `Unit → Grade · title, blurb, order`
- `Subject key, title (ریاضی، حسابان، هندسه، آمار و احتمال، گسسته)`
- `Unit → Grade, → Subject · title, blurb, order`
- `Node → Unit · order (جزیره)`
- `Lesson → Node · kind, example, steps`
- `Question → Lesson · prompt, type`
- `Lesson → Node · kind, example, steps, figures`
- `Question → Lesson · prompt, type, figure`
- `Choice → Question · value, is_correct`
- `SeedRecord دفترِ آنچه seed تحویل داده (تا کارِ ادمین برگردانده نشود)`
**content فعالیتِ کاربر**
- `UserAnswer user_id, → Question, → Choice`
- `UserLessonCompletion user_id, → Lesson`
- `UserNodeCompletion user_id, → Node, stars, xp_earned, coins_earned`
- `NodeProgress user_id, → Node, status`
- `QuestionReport «این سؤال ایراد دارد»`
- `ContentVote user_id، سؤال یا درس، ۱ / −۱`
- `QuestionReport · LessonReport «ایراد دارد»`
**progress بازی**
- `UserStats user_id, xp, coins, selected_character_id`
@@ -390,6 +398,7 @@ with transaction.atomic():
- `Subscription user_id, → Plan, status, expires_at`
- `Order user_id, plan_id, subscription_id, amount, list_price, plan_title, duration_days, status (… canceled, refunded), provider, gateway_authority, bazaar_purchase_token, bazaar_valid_until`
- `Transaction → Order · gateway_ref_id, status, paid_at`
- `PaymentEvent order_id, event, detail — هر قدمِ مسیرِ خرید`
- `BazaarCredential توکنِ اتصال به بازار`
**discount · affiliate بازاریابی**
@@ -421,7 +430,7 @@ with transaction.atomic():
| یکتا: `CampaignDelivery(کمپین، کاربر، کانال)` | پیامِ تکراری وقتی کمپین دو بار هم‌زمان اجرا شود |
| یکتا: `LessonChunk(collection, qdrant_point_id)` و `IngestedPage(collection, page)` | تکه‌ی تکراری در Qdrant وقتی خواندنِ کتاب از سر گرفته می‌شود |
> **کد و محتوا دو مسیرِ جدا دارند.** `db.sqlite3` در git نیست. محتوا (برنامه‌ی درسی و سؤال‌ها) به‌صورتِ JSON در `content/seed/` است و `deploy.sh` در هر انتشار آن را وارد می‌کند. داده‌ی کاربرها فقط در فایلِ دیتابیسِ سرور است.
> **کد و محتوا دو مسیرِ جدا دارند.** دیتابیس در git نیست. محتوا (برنامه‌ی درسی و سؤال‌ها) به‌صورتِ JSON در `content/seed/` است و `deploy.sh` در هر انتشار آن را وارد می‌کند. داده‌ی کاربرها فقط در Postgresِ سرور (و کپیِ زنده‌اش روی سرورِ بکاپ) است.
## ۸. API: قواعد و فهرستِ کامل
@@ -434,12 +443,13 @@ with transaction.atomic():
| نشانیِ پایه | `https://mathapi.tadrisino2.ir/api/…` — دامنه در کد نیامده و از تنظیمات ( `PUBLIC_BASE_URL` ) خوانده می‌شود. |
| ورود | هدرِ `Authorization: Bearer <token>`. توکن یک ساعت اعتبار دارد و با `POST /api/auth/refresh` تازه می‌شود. |
| قالبِ داده | JSON؛ نامِ فیلدها camelCase ( `selectedOptionId` )، شناسه‌ها در پاسخ رشته‌اند. |
| خطا | همیشه JSON با `detail` (و گاهی `code` مثلِ `no_hearts` ): ۴۰۰ ورودیِ بد، ۴۰۱ توکنِ نامعتبر، ۴۰۳ اجازه نداری، ۴۰۴، ۴۲۹ زیاد درخواست دادی، ۵۰۳ سرویسِ بیرونی در دسترس نیست. |
| خطا | همیشه JSON با `detail` (و گاهی `code` مثلِ `no_hearts`، `node_locked`، `answers_missing`، `subscription_required` ): ۴۰۰ ورودیِ بد، ۴۰۱ توکنِ نامعتبر، ۴۰۲ اشتراک لازم است، ۴۰۳ اجازه نداری، ۴۰۴، ۴۰۹ پیش‌شرط برقرار نیست، ۴۲۹ زیاد درخواست دادی، ۵۰۳ سرویسِ بیرونی یا ظرفیت در دسترس نیست (با `Retry-After` ). |
| متن | متنِ فارسیِ پاسخ‌ها بی‌اعراب و بی «ی»ِ اضافه است، و هیچ LaTeX یا markdownی ندارد. |
| تکرارِ امن | هدرِ اختیاریِ `Idempotency-Key` روی خرید، دستیار، جواب، پایانِ درس و جزیره: درخواستِ تکراری با همان کلید، همان نتیجه‌ی قبلی را برمی‌گرداند. |
| سقفِ نرخ | هر اندپوینتِ حساس یک `throttle_scope` دارد؛ نرخ‌ها همه یک‌جا در `config/settings.py → DEFAULT_THROTTLE_RATES`. |
| استریم | `POST /api/ai/ask` با `"stream": true` پاسخ را SSE می‌فرستد: رویدادهای `chunk`، `done`، `error`. |
### فهرستِ ۵۹ اندپوینت
### فهرستِ اندپوینت‌ها (۶۱ عملیات)
عمومی بی‌ورود · ورود توکن لازم · داخلی با هدرِ کلیدِ داخلی (سرور-به-سرور)
@@ -451,11 +461,11 @@ with transaction.atomic():
| POST | `/api/auth/refresh` | عمومی | توکنِ تازه با refresh token |
| POST | `/api/auth/logout` | ورود | خروج؛ توکن‌های قبلی باطل |
| GET · PATCH | `/api/user/profile` | ورود | خواندن و ویرایشِ پروفایل (تاریخِ تولدِ شمسی، شناسه‌ی نمایشی) |
| POST | `/api/user/delete` | ورود | حذفِ حساب؛ شماره‌دار با کدِ پیامکیِ تازه، مهمان با تایپِ «حذف» |
| POST | `/api/user/delete` | ورود | حذفِ حساب با کدِ پیامکیِ تازه |
| POST | `/api/user/grade` | ورود | ثبتِ پایه |
| **محتوا و آزمون — `content`** | | | |
| GET | `/api/grades` | عمومی | فهرستِ پایه‌ها |
| GET | `/api/home` | ورود | نقشه‌ی جزیره‌ها + آمار، در یک درخواست |
| GET | `/api/home` | ورود | نقشه‌ی جزیره‌ها + آمار، در یک درخواست؛ `?subject=` کتاب را انتخاب می‌کند |
| GET | `/api/nodes/{id}/lessons` | عمومی | درس‌های یک جزیره |
| GET | `/api/nodes/{id}/quiz` | عمومی | آزمونِ ۴ سؤالیِ جزیره |
| POST | `/api/nodes/{id}/quiz/complete` | ورود | پایانِ جزیره؛ ستاره، امتیاز، سکه، جزیره‌ی بعد |
@@ -463,7 +473,8 @@ with transaction.atomic():
| GET | `/api/lessons/{id}/questions` | عمومی | سؤال‌های یک درس |
| POST | `/api/lessons/{id}/complete` | ورود | پایانِ درس |
| POST | `/api/questions/{id}/answer` | ورود | ثبتِ جواب |
| POST | `/api/questions/{id}/report` | ورود | گزارشِ ایرادِ سؤال |
| POST | `/api/questions/{id}/report` · `/api/lessons/{id}/report` | ورود | گزارشِ ایرادِ سؤال یا درس |
| POST | `/api/questions/{id}/vote` · `/api/lessons/{id}/vote` | ورود | 👍/👎 روی سؤال یا درس ( `value` ۱، −۱ یا ۰) |
| GET | `/api/progress/nodes` | ورود | وضعیتِ خودِ کاربر روی جزیره‌ها |
| **پیشرفت و بازی — `progress`** | | | |
| GET | `/api/user/summary` | ورود | پروفایل + آمار + زنجیره در یک درخواست |
@@ -479,10 +490,10 @@ with transaction.atomic():
| POST | `/api/ai/messages/{id}/feedback` | ورود | 👍/👎 روی یک جواب |
| POST | `/api/rag/ingest/text` · `/pdf` | داخلی | افزودنِ متن یا PDF به Qdrant |
| GET | `/api/rag/chunks` | داخلی | مرورِ تکه‌های ذخیره‌شده |
| **بسته، پرداخت، تخفیف** | | | |
| **اشتراک، پرداخت، تخفیف** | | | |
| GET | `/api/subscription/plans` | عمومی | اشتراک‌های در حالِ فروش |
| POST | `/api/subscription/purchase` | ورود | شروعِ خرید؛ سفارش و لینکِ پرداخت |
| GET | `/api/subscription/status` | ورود | اعتبارِ سوال، قلبِ نامحدود تا کِی، اشتراکِ قدیمی (و بازسنجیِ پرداختِ جاافتاده) |
| GET | `/api/subscription/status` | ورود | اشتراکِ فعال و پایانش (و بازسنجیِ پرداختِ جاافتاده و تمدیدِ بازار) |
| POST | `/api/payment/bazaar/orders/{id}/start` | ورود | شروعِ خریدِ بازار (SKU و قیمتِ امضاشده) |
| GET | `/api/payment/orders/{id}` | ورود | وضعیتِ سفارش |
| GET | `/api/payment/invoices` | ورود | پرداخت‌ها و فاکتورهای کاربر (شماره‌ی فاکتور، تخفیف، کدِ پیگیری) |
@@ -501,7 +512,7 @@ with transaction.atomic():
| POST | `/api/support/bug-reports` | ورود | گزارشِ باگ |
| GET | `/api/user/weekly-report` | ورود | «گزارشِ والدین»: ۷ روزِ اخیر در کنارِ ۷ روزِ قبلش |
| **سیستم** | | | |
| GET | `/api/health` | عمومی | سلامتِ سرویس (بخشِ ۱۳) |
| GET | `/api/health` | عمومی (جزئیات با کلیدِ داخلی) | سلامتِ سرویس (بخشِ ۱۳) |
## ۹. دستیارِ هوشِ مصنوعی و داده‌ی Qdrant
@@ -513,14 +524,14 @@ Qdrant یک «بانکِ برداری» است: هر تکه‌ی کتاب به
| collection (کتاب) | تکه | صفحه‌ی خوانده‌شده | چه کسی ساخت |
|---|---|---|---|
| ریاضی هشتم | ۵۱۸ | ۱۵۰ | ما — خواندنِ PDF با مدلِ بینایی |
| ریاضی نهم | ۴۵۰ | ۱۴۳ | ما |
| ریاضی دهم | ۴۵۸ | ۱۷۰ | ما |
| حسابان یازدهم | ۳۷۶ | ۱۵۱ | ما |
| حسابان دوازدهم | ۳۲۶ | ۱۴۶ | ما |
| ریاضی چهارم · پنجم | ۵۹۰ · ۵۹۴ | ۱۵۴ · ۱۹۶ | ما — خواندنِ PDF با مدلِ بینایی |
| ریاضی هشتم · نهم · دهم | ۵۱۸ · ۴۵۰ · ۴۵۸ | ۱۵۰ · ۱۴۳ · ۱۷۰ | ما |
| حسابان یازدهم · دوازدهم | ۳۷۶ · ۳۲۶ | ۱۵۱ · ۱۴۶ | ما |
| هندسه دهم · یازدهم · دوازدهم | ۲۲۹ · ۱۷۱ · ۱۹۲ | ۸۸ · ۷۲ · ۷۸ | ما |
| آمار و احتمال یازدهم · گسسته دوازدهم | ۲۷۱ · ۲۵۴ | ۱۲۱ · ۸۴ | ما |
| ریاضی سوم · ششم · هفتم | ۴۳۶ · ۴۳۷ · ۳۷۹ | — | تیمِ دیگرِ شرکت؛ با `rag_import_collection` خوانده شد |
| علوم تجربی سوم · مطالعات اجتماعی ششم و چهارم | ۷۰ · ۴۰ · ۲۷ | — | تیمِ دیگر (در اپِ ریاضی استفاده نمی‌شود) |
| جمع | ۳٬۵۱۷ | ۷۶۰ | نشانی: `qdrant.tadrisino2.ir` — سرویسِ مشترک با تیمِ دیگر |
| جمع | ۵٬۸۱۸ | ۱٬۷۵۳ | نشانی: `qdrant.tadrisino2.ir` — سرویسِ مشترک با تیمِ دیگر |
#### مشخصاتِ فنی
@@ -555,7 +566,7 @@ Qdrant یک «بانکِ برداری» است: هر تکه‌ی کتاب به
5. LessonChunk ردیف در دیتابیس (منبعِ اصلی، با بردار)
6. Qdrant upsert نقطه + payload IngestedPage برای ازسرگیری
قاعده‌ی مهمِ پرامپت ( `rag/services/pdf.py` ): اگر مسئله به «شکلِ زیر» ارجاع می‌دهد، مدل باید اندازه‌ها و برچسب‌های شکل را **داخلِ متن** بنویسد. این قاعده سهمِ مسئله‌های بی‌استفاده‌ی وابسته به شکل را از ~۴۲٪ به ~۱۴٪ رساند. هزینه‌ی خواندنِ هر پنج کتاب در یک روز حدودِ ۳٫۴۴ دلار بود.
قاعده‌ی مهمِ پرامپت ( `rag/services/pdf.py` ): اگر مسئله به «شکلِ زیر» ارجاع می‌دهد، مدل باید اندازه‌ها و برچسب‌های شکل را **داخلِ متن** بنویسد. این قاعده سهمِ مسئله‌های بی‌استفاده‌ی وابسته به شکل را از ~۴۲٪ به ~۱۴٪ رساند. هزینه‌ی خواندنِ پنج کتابِ اول در یک روز حدودِ ۳٫۴۴ دلار بود.
### ۹-۳ کنترلِ کیفیت: راستی‌آزماییِ راه‌حل‌های کتاب
@@ -563,10 +574,10 @@ Qdrant یک «بانکِ برداری» است: هر تکه‌ی کتاب به
| رأی | تعداد | اثر |
|---|---|---|
| agrees راه‌حلِ کتاب درست است | ۲٬۴۶۶ | فقط از این‌ها سؤال ساخته می‌شود |
| disagrees راه‌حلِ کتاب غلط است | ۳۵۴ | راه‌حلش به دستیار داده نمی‌شود؛ فقط روش و صورتِ مسئله |
| unjudgeable بی‌شکل قابلِ قضاوت نیست | ۵۰۸ | کنار می‌ماند |
| بررسی‌نشده | ۱۸۹ | علوم و مطالعاتِ تیمِ دیگر |
| agrees راه‌حلِ کتاب درست است | ۴٬۱۹۱ | فقط از این‌ها سؤال ساخته می‌شود |
| disagrees راه‌حلِ کتاب غلط است | ۶۴۴ | راه‌حلش به دستیار داده نمی‌شود؛ فقط روش و صورتِ مسئله |
| unjudgeable بی‌شکل قابلِ قضاوت نیست | ۷۴۲ | کنار می‌ماند |
| بررسی‌نشده | ۲۴۱ | بیشترش علوم و مطالعاتِ تیمِ دیگر |
متنِ اصلیِ کتاب هیچ‌وقت بازنویسی نمی‌شود؛ جوابِ مستقل جدا در `checked_answer` ذخیره می‌شود تا آدم بتواند داوری کند.
@@ -591,9 +602,9 @@ Qdrant یک «بانکِ برداری» است: هر تکه‌ی کتاب به
|---|---|---|
| کدام کتاب؟ | `rag/services/lesson_context.py` | از `lessonId` زنجیره‌ی درس → جزیره → فصل → پایه؛ یا پایه‌ی خودِ کاربر |
| جست‌وجو | `retrieval.py` · `qdrant_store.py` · `embedding.py` | متنِ جست‌وجو = سؤال + سؤالِ قبلیِ کاربر (برای «نفهمیدم») |
| پرامپت | `generation.py` | نسخه‌ی `tutor-v10-shape-anchor` ؛ لحنِ سه‌سطحی (دبستان/متوسطه‌ی اول/دبیرستان)؛ متنِ ساده بی‌LaTeX؛ حلِ مرحله‌به‌مرحله با وارسی و «✅ جواب»؛ ۶ پیامِ آخرِ گفتگو |
| پرامپت | `generation.py` | نسخه‌ی `tutor-v12-age-tone` ؛ فقط ریاضی؛ لحنِ سه‌سطحی (دبستان/متوسطه‌ی اول/دبیرستان) و لحنِ بزرگسال برای ۱۸+؛ متنِ ساده بی‌LaTeX؛ حلِ مرحله‌به‌مرحله با وارسی و «✅ جواب»؛ ۶ پیامِ آخرِ گفتگو |
| جوابِ درست/غلطِ دانش‌آموز | `generation.py → question_focus` | با `questionId` دستیار صورتِ سؤال و آخرین جوابِ دانش‌آموز را می‌بیند و می‌گوید اشتباه کجا بود |
| تمیزکاری | `mathtext.py` | هرچه LaTeX از دستِ مدل در برود به یونیکد برمی‌گردد ( `x²`، `√3` ) |
| تمیزکاری | `mathtext.py` | هرچه LaTeX از دستِ مدل در برود به یونیکد برمی‌گردد ( `x²`، `√3` ) و اعراب حذف می‌شود |
| شکل | `diagram.py` ← فرانت `src/diagram/` | مدل فقط توصیفِ JSON می‌نویسد؛ سرور جدا، تعمیر و با فهرستِ سفید وارسی می‌کند؛ اپ مختصات را حساب و SVG رسم می‌کند. ۱۵ نوع شکل — شرحِ کامل در کادرِ بنفشِ پایین. |
| ثبت | `ai_tutor/models.py → Message` | مدل، نسخه‌ی پرامپت، تعدادِ تکه، تأخیر و شکل برای هر جواب ذخیره می‌شود ← کیفیت قابلِ اندازه‌گیری است |
@@ -698,7 +709,7 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
| انتخابِ مدلِ جواب: ۱۰۲ مسئله‌ی تصحیح‌شده‌ی دستی | `gpt-4.1-mini` : ۹۵–۹۶ درست · `gpt-4o-mini` : ۸۵–۹۰ |
| شکل‌ها: ۶۴ مسئله‌ی تصادفیِ کتاب، وارسیِ چشمیِ تک‌تک | ۴۰ درست، ۱۲ قابلِ قبول، ۷ غلط، ۵ بی‌شکل — حدودِ یکی از پنج هنوز ایراد دارد؛ برای همین شکل همیشه کنارِ حلِ متنی می‌آید |
| سرعت | جوابِ کامل با شکل ~۳۰ ثانیه؛ اندازه‌گیری شد که در خودِ GapGPT است («فقط بگو سلام» = ۷ ثانیه) |
| سقف‌ها | ۲ سؤالِ رایگان در روز (مشترک ۴)؛ ۲۰ در ساعت؛ مهمان‌ها بر اساسِ IP (تا با ساختِ حسابِ مهمانِ تازه دور زده نشود) |
| سقف‌ها | ۲ سؤالِ رایگان در روز (مشترک ۴)؛ ۲۰ در ساعت؛ ۳٬۰۰۰ در روز برای کلِ سامانه؛ حداکثر ۱۲ جوابِ هم‌زمان در هر worker (بیشتر از آن ۵۰۳ «دستیار شلوغه» پیش از کم شدنِ سهمیه) |
### ۹-۷ فقط ریاضی، و چه چیزی هزینه دارد
@@ -720,12 +731,12 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
| فایل | محتوا |
|---|---|
| `content/seed/curriculum.json` | ۸ پایه، ۵۸ فصل، ۲۶۳ جزیره، ۲٬۶۱۷ درس |
| `content/seed/questions.json` | ۲٬۵۴۵ سؤالِ چهارگزینه‌ای |
| `content/seed/curriculum.json` | ۱۰ پایه، ۵ کتاب، ۸۹ فصل، ۱٬۱۴۵ جزیره، ۵٬۴۰۶ درس، با مثال، قدم‌ها و شکلِ درس |
| `content/seed/questions.json` | ۱۸٬۵۲۲ سؤالِ چهارگزینه‌ای، بعضی با شکل |
- `seed_content` در هر انتشار اجرا می‌شود؛ **هیچ‌وقت چیزی پاک نمی‌کند** و اجرای دوباره‌اش بی‌خطر است.
- سؤال با **مسیرِ عنوان** (پایه › فصل › جزیره › درس) به درس وصل می‌شود، نه با شناسه — چون شناسه‌ها بینِ دو دیتابیس فرق دارند.
- متنی که ادمین در پنل نوشته با انتشارِ بعدی بازنویسی نمی‌شود؛ seed فقط خانه‌ی خالی را پر می‌کند.
- کارِ ادمین با انتشارِ بعدی برنمی‌گردد: دفترِ `SeedRecord` به یاد دارد seed چه چیزی تحویل داده، پس سؤالِ پاک‌شده یا فصلِ تغییرنام‌داده دوباره ساخته نمی‌شود؛ مثال، قدم‌ها و شکل فقط خانه‌ی خالی را پر می‌کنند.
### کارهای روزمره از پنلِ ادمین
@@ -736,7 +747,7 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
| خروجیِ سؤال یا برنامه‌ی درسی | انتخاب ← اکشنِ «دریافتِ فایل» (همان قالبی که ورودی می‌پذیرد) |
| پایه‌ی تازه | `/admin/content/grade/add/` — عنوانِ «پایه چهارم»، شناسه خودکار ۴ |
| پلن، همراه، تیکت، کدِ تخفیف | صفحه‌ی اپِ مربوط در پنل |
| سؤال‌های گزارش‌شده | `QuestionReport` — دانش‌آموز «این سؤال ایراد دارد» زده |
| بازخوردِ دانش‌آموز | ستونِ 👍/👎 در فهرستِ سؤال‌ها و درس‌ها؛ صفِ «گزارش‌های سوال» و «گزارش‌های درس» |
### سؤال‌ها چطور ساخته شدند
@@ -748,7 +759,9 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
بازده حدودِ ۸۷–۹۲٪ بود؛ بقیه عمداً رد شدند (بیشتر مسئله‌هایی که بی‌شکلِ کتاب خودکفا نمی‌شوند). همین مسیر با API پولی هم ممکن است ولی اندازه‌گیری‌شده ~۰٫۷۷ سنت برای هر سؤال گران‌تر بود بی‌کیفیتِ بهتر. نویسنده‌های سؤال چند بار خطای حسابِ خودِ کتاب را هم گرفتند.
**پوششِ فعلی:** هر جزیره‌ی هر ۸ پایه دستِ‌کم ۴ سؤال دارد. ۵۹۰ درس و ۵۸ فصل متنِ معرفی و مثال دارند ( `fill_lesson_texts`، بی‌هزینه، هرگز همان مسئله‌ی آزمون).
**جزیره‌های تمرین:** آخرِ هر فصل از پایه‌ی سوم تا دوازدهم یک تا چند جزیره‌ی «تمرین» است (۱۶۰ جزیره، ~۱۳٫۷ هزار سؤال). جوابِ این سؤال‌ها با برنامه **محاسبه** شده، نه حفظ، و گزینه‌های غلط از جنسِ خطای رایجِ دانش‌آموزند. قالبِ سؤال‌های هر خانواده یکی است و فقط عددها عوض می‌شوند؛ بازبینیِ معلم هنوز انجام نشده است.
**شکلِ درس:** درس‌های هندسه، آمار و گسسته شکلِ ثابت دارند ( `Lesson.figures` )، دستی نوشته و با چشم بازبینی‌شده؛ هنگامِ نمایش هیچ مدلی صدا زده نمی‌شود.
## ۱۱. گیمیفیکیشن و همراه‌ها
@@ -776,10 +789,11 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
| امتیاز (XP) | پایانِ درس یا جزیره: **۱۰ × جوابِ درست**، فقط بارِ اول. عددِ جوابِ درستی که اپ می‌فرستد به جواب‌های درستِ **ثبت‌شده** محدود می‌شود؛ صدا زدنِ مستقیمِ API امتیازِ مجانی نمی‌دهد. | `content/views.py → NodeQuizCompleteView` |
| ستاره | ۹۰٪ به بالا ۳ · ۶۰٪ به بالا ۲ · بیشتر از صفر ۱ | همان |
| سکه | **۵ × ستاره**، فقط بارِ اول | همان |
| قلب | ۵ قلب؛ هر جوابِ غلط یکی کم؛ هر **۵ ساعت** یکی برمی‌گردد و **۲۴ ساعت** بعد از اولین قلبِ ازدست‌رفته همه پر می‌شوند. با قلبِ صفر نه جوابی پذیرفته می‌شود نه پایانِ درس یا جزیره‌ی بی‌جواب ( `403 no_hearts` ). اشتراکِ فعال یا روزهای قلبِ بسته = قلبِ نامحدود. | `progress/models.py → UserHearts` · `content/views.py` |
| قلب | ۵ قلب؛ هر جوابِ غلط یکی کم؛ هر **۵ ساعت** یکی برمی‌گردد و **۲۴ ساعت** بعد از اولین قلبِ ازدست‌رفته همه پر می‌شوند. با قلبِ صفر نه جوابی پذیرفته می‌شود نه پایانِ درس یا جزیره‌ی بی‌جواب ( `403 no_hearts` ). اشتراکِ فعال = قلبِ نامحدود. | `progress/models.py → UserHearts` · `content/views.py` |
| زنجیره (Streak) | جواب، پایانِ درس یا جزیره امروز را (به وقتِ تهران) روزِ فعال می‌کند. اگر آخرین روزِ فعال نه امروز باشد نه دیروز، صفر نشان داده می‌شود. | `progress/services.py` |
| جدولِ رتبه | هفتگی (امتیازِ ۷ روزِ اخیر) و همه‌ی زمان‌ها؛ زنده حساب و ۶۰ ثانیه کش می‌شود. | `progress/leaderboard.py` |
| پیشرفت روی نقشه | پایانِ آزمون، جزیره را با هر نمره‌ای تمام می‌کند و جزیره‌ی بعدی ( `nextNodeId` ) باز می‌شود؛ نمره فقط ستاره و امتیاز را تعیین می‌کند (تصمیمِ محصولی: ساده بماند). | `content/services.py` |
| پیشرفت روی نقشه | پایانِ آزمون، جزیره را با هر نمره‌ای تمام می‌کند و جزیره‌ی بعدی ( `nextNodeId` ) باز می‌شود؛ نمره فقط ستاره و امتیاز را تعیین می‌کند (تصمیمِ محصولی: ساده بماند). جزیره‌ی قفل ( `403 node_locked` ) و آزمونِ جواب‌نداده ( `409 answers_missing` ) پذیرفته نمی‌شوند. آزمونِ هر جزیره برای هر دانش‌آموز ثابت است. | `content/views.py` |
| دیوارِ پرداخت | فصلِ اولِ هر کتاب رایگان؛ بقیه با اشتراکِ فعال ( `402 subscription_required` ). | `content/views.py → unit_is_free` |
| اعلان | «جزیره را با ۳ ستاره تمام کردی و ۴۰ امتیاز گرفتی» — با رقمِ فارسی. | `notifications/services.py` |
> **قلب واقعی است** ( `HEARTS_ENFORCED` پیش‌فرض روشن): قلبِ صفر بازی را متوقف می‌کند تا قلبِ بعدی برگردد. چکِ قلب داخلِ همان تراکنشی است که قلب را کم می‌کند، پس دو جوابِ هم‌زمان با یک قلبِ باقی‌مانده هر دو پذیرفته نمی‌شوند؛ و پایانِ آزمون بی‌جواب دادن هم بسته است. کسی که آخرین قلبش را روی آخرین سوال از دست داده، جزیره را تمام می‌کند. تست‌ها: `content/tests_hearts_gate.py` .
@@ -790,7 +804,7 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
## ۱۲. امنیت و احرازِ هویت
سه دور بازبینیِ بدبینانه روی کلِ بک‌اند انجام شد (مرداد، ۳ مهر و ۱۰ مهر)، و یک وارسیِ نهایی روی سایتِ زنده (۷ مهر) با این سؤال: «یک دانش‌آموزِ متقلب، یک مهاجم یا یک والدِ سردرگم چطور این را می‌شکند؟» هر یافته با یک تست بسته شد. این بخش صادقانه می‌گوید چه بسته است و چه هنوز باز.
چند دور بازبینیِ بدبینانه روی کلِ بک‌اند انجام شد (مرداد، ۳، ۱۰ و ۲۱ مهر)، با وارسی روی سایت و سرورِ زنده، با این سؤال: «یک دانش‌آموزِ متقلب، یک مهاجم یا یک والدِ سردرگم چطور این را می‌شکند؟» هر یافته با یک تست بسته شد. این بخش صادقانه می‌گوید چه بسته است و چه هنوز باز.
### ۱۲-۱ هویت و توکن
@@ -806,36 +820,33 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
| حوزه | چه کرده‌ایم | کجا |
|---|---|---|
| دسترسی به داده‌ی دیگران | شناسه‌ی کاربر همیشه از توکن خوانده می‌شود، هرگز از ورودی؛ هر کاربر فقط داده‌ی خودش را می‌بیند (گزارشِ والدین هم در حسابِ خودِ دانش‌آموز است). | همه‌ی `views.py` ها |
| سقفِ نرخ | روی همه‌ی نقاطِ حساس (ورود، دستیار، خرید، کدِ تخفیف، جواب…)؛ مهمان بر اساسِ IPِ واقعی ( `NUM_PROXIES=2` : آروان + nginx). | `config/settings.py` |
| پیامک و هزینه‌اش | شماره به `09XXXXXXXXX` نرمال می‌شود؛ فاصله بینِ دو درخواست، ۱۰ در روز برای هر شماره، ۳۰۰ در روز برای کلِ سامانه (ضدِ SMS pumping) — شمارش در دیتابیس، نه حافظه‌ی موقت. | `accounts/phone.py` · `tests_otp_limits.py` |
| سقفِ نرخ | روی همه‌ی نقاطِ حساس (ورود، دستیار، خرید، کدِ تخفیف، جواب…)؛ درخواستِ بی‌ورود بر اساسِ IPِ واقعی، که فقط از پروکسی‌های آروان پذیرفته می‌شود (جعلِ `X-Forwarded-For` اثری ندارد). nginx هر درخواستِ نرسیده از آروان را رد می‌کند. | `config/middleware.py → TrustedProxyMiddleware` · `ops/nginx/` |
| پیامک و هزینه‌اش | شماره به `09XXXXXXXXX` نرمال می‌شود؛ فاصله بینِ دو درخواست، ۱۰ در روز برای هر شماره، سقفِ روزانه برای کلِ سامانه (روی سرور ۲٬۰۰۰، ضدِ SMS pumping) — شمارش در دیتابیس، نه حافظه‌ی موقت. | `accounts/phone.py` · `tests_otp_limits.py` |
| پرداخت | فقط تأییدِ سرور-به-سرور؛ برگشتِ جعلی اثری ندارد؛ webhookِ عمومیِ ناامن حذف شد؛ هر درگاهی بی‌کلید «بسته» می‌ماند (fail-closed)، نه موفقیتِ ساختگی. هر سفارش فقط یک بار روز می‌دهد (قیدِ یکتا روی تراکنش)؛ وضعیتِ سفارش در ادمین فقط با actionِ لغو/بازپرداخت عوض می‌شود. | `payment/` · `subscription/` |
| تقلبِ امتیاز | امتیاز فقط برای جوابِ ثبت‌شده؛ جایزه فقط بارِ اول. | `content/views.py` |
| درخواستِ تکراری و هم‌زمانی | Idempotency-Key در شش نقطه؛ `transaction.atomic` ؛ قیدِ یکتا در دیتابیس؛ SQLite در حالتِ `IMMEDIATE` تا قفل‌ها واقعاً قفل کنند. | `config/settings.py` |
| درخواستِ تکراری و هم‌زمانی | Idempotency-Key در شش نقطه؛ `transaction.atomic` ؛ قیدِ یکتا در دیتابیس؛ قفلِ ردیفیِ Postgres. | `config/settings.py` |
| خطا و لاگ | هر خطا JSONِ یکدست بی‌جزئیاتِ داخلی؛ لاگ بدونِ بدنه، هدر، توکن یا رمز. | `config/exceptions.py` · `middleware.py` |
| محتوای مدل | HTML یا SVGِ تولیدِ مدل هرگز واردِ صفحه نمی‌شود؛ فقط JSONِ وارسی‌شده با فهرستِ سفید. دستیار فقط ریاضی (بخشِ ۹-۷). | `rag/services/diagram.py` · `guardrails.py` |
| حذفِ حساب | شماره‌دار با کدِ پیامکیِ تازه (همان سقفِ حدسِ ورود)، مهمان با تایپِ «حذف»؛ حسابِ نمایشی حذف نمی‌شود؛ سفارش‌ها برای حسابداری می‌مانند؛ توکنِ قبلی بعد از حذف کار نمی‌کند. | `accounts/views.py → DeleteAccountView` |
| مرورگر | CORS فقط برای دامنه‌ی فرانت؛ پاسخِ API با `no-store` در کشِ آروان نمی‌ماند؛ نامِ نمایشی از شماره و لینک پاک می‌شود؛ سقفِ حدسِ رمزِ ادمین برای هر IP. | `config/middleware.py` |
| کلیدها | همه در `.env` ِ سرور؛ هیچ کلیدی در git یا در پاسخِ API نیست؛ `/api/health` فقط «بله/خیر» می‌گوید. | `.env.example` |
| حذفِ حساب | با کدِ پیامکیِ تازه (همان سقفِ حدسِ ورود)؛ سفارش‌ها برای حسابداری می‌مانند؛ توکنِ قبلی بعد از حذف کار نمی‌کند. | `accounts/views.py → DeleteAccountView` |
| مرورگر | CORS فقط برای دامنه‌ی فرانت؛ CSP و هدرهای امنیتی روی سایتِ فرانت؛ پاسخِ API با `no-store` در کشِ آروان نمی‌ماند؛ نامِ نمایشی از شماره و لینک پاک می‌شود؛ سقفِ حدسِ رمزِ ادمین برای هر IP. | `config/middleware.py` · `ops/nginx/` |
| کلیدها | همه در `.env` ِ سرور؛ کلیدِ امضای توکن روی خودِ سرور ساخته شده و در هیچ مخزنی نیست؛ `/api/health` بی‌کلیدِ داخلی فقط «ok/error» می‌گوید و Swagger فقط برای ادمینِ واردشده باز است. هیچ ورودِ آزمایشی با کدِ ثابت روی سرور نیست. | `.env.example` |
### ۱۲-۳ آنچه عمداً باز مانده (پیش از کاربرِ واقعی باید بسته شود)
### ۱۲-۳ آنچه هنوز باز است (پیش از کاربرِ عمومی باید بسته شود)
این موارد در مرحله‌ی نمایشی (دمو) برای کارِ تیم عمداً باز گذاشته شده‌اند؛ فهرستِ کامل با خطر و راهِ بستنِ هرکدام در `docs/security-before-production.md` است (۱۸ مورد).
فهرستِ کامل با خطر و راهِ بستنِ هرکدام در `docs/security-before-production.md` است؛ مهم‌ترین‌ها:
| # | مورد | خطر | راهِ بستن |
|---|---|---|---|
| ۱ | کلیدِ امضای توکن روی سرور همان کلیدِ پیش‌فرضِ داخلِ مخزن است؛ هر کس مخزن را ببیند می‌تواند توکن جعل کند. | بحرانی | حذف از `.env` تا تنظیمات خودش کلیدِ تصادفی بسازد (کد آماده است)؛ همه یک بار دوباره وارد می‌شوند. |
| ۲ | ۷ شماره‌ی دمو با کدِ ثابت روی سرورِ واقعی | بالا | پاک کردنِ `TEST_LOGIN_PHONES` |
| ۳ | داده و حساب‌های دمو و آزمایشی در دیتابیسِ واقعی | بالا | پاک‌سازی پس از بکاپ |
| ۴ | دسترسیِ سرور: حسابِ مشترک با رمز و sudoِ کامل | بالا | کلیدِ SSH و حسابِ محدود — کارِ ادمینِ سرور |
| ۵ | `/api/health`، Swagger و جدولِ رتبه بی‌ورود عمومی‌اند؛ پنلِ ادمین روی دامنه‌ی عمومی | متوسط | محدود کردن با ورود یا IP |
| ۶ | سرور بی‌آروان هم روی پورتِ ۸۰ جواب می‌دهد (امکانِ جعلِ IP برای دور زدنِ سقف) | متوسط | بستنِ پورت جز برای آروان — ادمینِ سرور |
| ۷ | کشِ مشترک (Redis) تنظیم نشده؛ سقف‌های نرخ برای هر worker جداست | متوسط | Redisِ اختصاصیِ خودمان (به Redisِ مشترکِ ماشین وصل نمی‌شویم؛ تصمیمِ باز) |
| ۸ | کدِ پیامکی و refresh token بی‌هش در دیتابیس؛ توکن در `localStorage` ِ مرورگر | کم | هش کردن؛ کوکیِ httpOnly |
| ۹ | آزمونِ جزیره قفل بودنِ جزیره را نمی‌سنجد (می‌شود جزیره‌ی جلوتر را با API تمام کرد) | کم | بررسیِ جزیره‌ی قبلی در `NodeQuizCompleteView` |
| مورد | خطر | راهِ بستن |
|---|---|---|
| سقفِ روزانه‌ی پیامکِ کلِ سامانه را یک ناشناس با شماره‌های تصادفی پر می‌کند و ورود برای همه بسته می‌شود | بالا | CAPTCHA یا مانعِ مشابه پس از یک حد؛ شمارش بر اساسِ شبکه — تصمیمِ طراحی |
| دسترسیِ سرور: حسابِ مشترک با رمز و sudoِ کامل | بالا | کلیدِ SSH و حسابِ محدود — کارِ ادمینِ سرور |
| پنلِ `/admin/` روی دامنه‌ی عمومی (با سقفِ تلاش، بی ۲FA) | متوسط | محدود به IP یا VPN، ۲FA |
| با دانستنِ شماره‌ی کسی، ورودِ او تا ۲۴ ساعت بسته می‌شود | متوسط | گره زدنِ کد به درخواست‌کننده — تصمیمِ طراحی |
| کدِ پیامکی بی‌هش در دیتابیس؛ توکن در `localStorage` ِ مرورگر | کم | هش کردن؛ کوکیِ httpOnly |
| کلیدِ جوابِ هر سوال با حسابِ دوم درمی‌آید | کم | گزینه‌ی درست فقط پس از پایانِ آزمون — تصمیمِ محصولی |
> #### جمع‌بندیِ وضعیتِ امنیت
>
> لایه‌ی برنامه بازبینی و تست شده است: دسترسی، سقفِ نرخ، پرداخت، تکرارِ درخواست و تقلبِ امتیاز بسته‌اند. چیزهایی که باز مانده عمدی و مستند است — مهم‌ترینِ آن‌ها کلیدِ امضای سرور است که کدِ رفعش آماده است و اعمالش به زمانی موکول شده که خروجِ همه‌ی کاربرها از حساب مشکلی ایجاد نکند. بخشی از موارد نیز به زیرساخت مربوط است و بر عهده‌ی ادمینِ سرور است.
> لایه‌ی برنامه بازبینی و تست شده است: دسترسی، سقفِ نرخ، جعلِ IP، پرداخت، تکرارِ درخواست و تقلبِ امتیاز بسته‌اند؛ کلیدِ امضا عوض شده، حساب‌ها و ورودهای نمایشی پاک شده‌اند و nginx فقط از آروان می‌پذیرد. آنچه مانده بیشتر تصمیمِ طراحی (پیامک و ورود) یا کارِ ادمینِ سرور است.
## ۱۳. استقرار (Deploy) و نگهداری
@@ -850,7 +861,7 @@ ssh -t administrator@<server> 'cd ~/mathapp-v2 && ./deploy.sh; sudo systemctl re
1. ۱ · fetch از Gitea
2. ۲ · reset تغییرِ دستی ← patch در .deploy-backups/
3. ۳ · pip install
4. ۴ · migrate
4. ۴ · بکاپِ دیتابیس (pg_dump) و migrate
5. ۵ · seed_content محتوا
6. ۶ · collectstatic
7. ۷ · restart + اثباتِ ری‌استارت
@@ -874,11 +885,12 @@ fetch ← `npm ci` ← بررسیِ نوع‌ها ← build در `dist.next` ←
|---|---|
| `PUBLIC_BASE_URL` · `FRONTEND_URL` | دامنه‌های فعلی. همه‌ی تنظیماتِ دامنه (CORS، callbackِ زرین‌پال، …) از همین دو ساخته می‌شوند؛ جابه‌جاییِ دامنه یعنی عوض کردنِ همین دو. |
| `DJANGO_SECRET_KEY` | یک کلیدِ تصادفی ساخته و در `.django_secret_key` نگه داشته می‌شود (حالتِ مطلوب). |
| `OPENAI_API_KEY` · `QDRANT_API_KEY` | دستیار جوابِ ثابتِ نمایشی می‌دهد. |
| `OPENAI_API_KEY` · `QDRANT_API_KEY` | دستیار ۵۰۳ صریح می‌دهد؛ جوابِ نمایشی فقط در حالتِ توسعه. |
| `KAVENEGAR_API_KEY` · `KAVENEGAR_OTP_TEMPLATE` | درخواستِ کد ۵۰۳ صریح می‌گیرد. الگو باید `SMSVerify` بماند. |
| `ZARINPAL_MERCHANT_ID` · `BAZAAR_*` | پرداخت بسته می‌ماند (۵۰۳). |
| `CACHE_URL` | کشِ جدا برای هر worker؛ `/api/health` هشدار می‌دهد. |
| `HEARTS_ENFORCED` · `TEST_LOGIN_PHONES` | روشن (قلب واقعی؛ `0` خاموشش می‌کند) · هیچ حسابِ آزمایشی. |
| `CACHE_URL` | روی سرور به Redisِ اختصاصیِ خودمان اشاره می‌کند. بی آن کش برای هر worker جداست و `/api/health` هشدار می‌دهد. |
| `DB_NAME` · `DB_USER` · `DB_PASSWORD` · `DB_HOST` · `DB_PORT` · `DB_POOL_MAX_SIZE` | Postgres و اندازه‌ی poolِ هر worker. بی `DB_NAME` برنامه SQLite را برمی‌دارد (فقط برای محلی). |
| `HEARTS_ENFORCED` · `TEST_LOGIN_PHONES` | روشن (قلب واقعی؛ `0` خاموشش می‌کند) · خالی؛ ورودِ آزمایشی فقط با کدِ یک‌بارمصرفی که ابرکاربر در پنل می‌سازد. |
| `AI_DAILY_QUOTA_FREE` | ۲ سوالِ رایگان در روز (پیش‌فرض). |
| `DJANGO_DEBUG` | خاموش — و **هرگز روی سرور روشن نشود** (کدِ ورودِ همگانیِ `0000` را فعال می‌کند). |
@@ -888,18 +900,20 @@ fetch ← `npm ci` ← بررسیِ نوع‌ها ← build در `dist.next` ←
> #### `GET /api/health`
>
> بی‌ورود و بی‌SSH می‌گوید: دیتابیس سالم است؟ migrationِ جامانده هست؟ کش کار می‌کند؟ چند پایه/درس/سؤال؟ کدام سرویسِ بیرونی تنظیم است؟ اعتبارِ دستیار تمام شده ( `aiProvider` )؟ هشدارهای امنیتی و ترافیکِ اخیر. ۲۰۰ = سالم، ۵۰۳ = خرابیِ واقعی.
> بی‌ورود فقط «ok/error» می‌گوید. با هدرِ `X-Internal-Key`: دیتابیس سالم است؟ migrationِ جامانده هست؟ کش و poolِ اتصال؟ کپیِ زنده‌ی بکاپ عقب نیست؟ کدام سرویسِ بیرونی تنظیم است و اعتبارِ دستیار تمام شده ( `aiProvider` )؟ هشدارهای امنیتی و ترافیکِ اخیر. ۲۰۰ = سالم، ۵۰۳ = خرابیِ واقعی.
> #### بکاپ
>
> دیتابیس در git نیست و تنها نسخه‌ی داده‌ی کاربرهاست. بکاپ با `sqlite3.Connection.backup()` ِ پایتون (نه `cp` ) پیش از هر انتشارِ مهم، در `.deploy-backups/`. هنوز خودکار نیست — ارزشمندترین کارِ بعدی.
> هر تغییرِ دیتابیس لحظه‌ای به سرورِ دوم (۲۴۷) می‌رود ( `pg_receivewal` از راهِ تونلِ محدود)، با یک نسخه‌ی کاملِ روزانه؛ بازیابی تا هر لحظه از ۷ روزِ اخیر ممکن است و آزموده شده. `deploy.sh` هم پیش از هر `migrate` یک `pg_dump` می‌گیرد. دفترِ رویدادهای خرید هر ۶ ساعت به CSV و لاگِ برنامه هر شب فشرده و برای همیشه روی ۲۴۷ نگه داشته می‌شود. رویه: `docs/operations.md`.
> #### سرور مالِ ما نیست؛ ما یکی از مستأجرهاییم
>
> - مالِ ما فقط `/home/administrator/mathapp-v2` و سرویسِ `zareian-mathapp` است. `gunicorn` ِ خالی سرویسِ تیمِ دیگری است.
> - هرگز `git clean` یا «پاک کن و از نو clone کن»: `db.sqlite3`، `.env` و کلیدِ امضا در همان پوشه‌اند و در git نیستند.
> - پیش از هر کارِ پرحجم `df -h /` : دیسک حدودِ ۹۵٪ پر است و پر شدنش Gitea و ایمیلِ شرکت را هم می‌خواباند.
> - مالِ ما فقط پوشه‌های `~/mathapp-v2` و `~/mathapp_frontend_new`، سرویس‌های `zareian-*` و clusterِ Postgresِ `zareian` است. `gunicorn` ِ خالی سرویسِ تیمِ دیگری است.
> - هرگز `git clean` یا «پاک کن و از نو clone کن»: `.env`، کلیدِ امضا، لاگ‌ها و بکاپ‌ها در همان پوشه‌اند و در git نیستند.
> - nginx مشترک است: `nginx -t` و بعد `reload`، هرگز `restart`.
> - پیش از هر کارِ پرحجم `df -h /` : پر شدنِ دیسک Gitea و ایمیلِ شرکت را هم می‌خواباند.
> - `.env` فقط از راهِ جنگو خوانده شود؛ مقدارش هرگز در چت یا گزارش نیاید.
> - قواعدِ کامل برای هر کسی (یا agentی) که روی سرور کار می‌کند: `CLAUDE.md` ، بخشِ «Working on the servers».
## ۱۴. مقیاس‌پذیری و پیوستنِ توسعه‌دهنده‌ی تازه
@@ -912,7 +926,7 @@ fetch ← `npm ci` ← بررسیِ نوع‌ها ← build در `dist.next` ←
| ۱۴ اپ با مرزِ روشن | هر نفر صاحبِ چند اپ می‌شود (مثلاً یکی پرداخت و اشتراک، یکی محتوا و گیمیفیکیشن، یکی دستیار)؛ تغییرِ هر کس کمتر به فایلِ دیگری می‌خورد. |
| بی‌FK بینِ اپ‌ها | تغییرِ جدول‌های یک اپ migrationِ اپِ دیگر را نمی‌شکند؛ هر اپ بعداً قابلِ جدا شدن به سرویسِ مستقل است. |
| قراردادِ OpenAPI | فرانت و بک‌اند بی‌جلسه روی یک شکلِ داده توافق دارند؛ `tests_frontend_contract.py` تغییرِ ناخواسته را می‌گیرد. |
| ۷۷۹ تستِ خودکار | تازه‌وارد بی‌ترس تغییر می‌دهد: اگر چیزی را شکست، تست قبل از سرور خبر می‌دهد. |
| ۹۷۳ تستِ خودکار | تازه‌وارد بی‌ترس تغییر می‌دهد: اگر چیزی را شکست، تست قبل از سرور خبر می‌دهد. |
| قواعدِ یک‌جا | نرخ‌ها، دامنه‌ها و تنظیمات فقط در `config/settings.py` ؛ پیامِ خطا، لاگ و هویت یک مسیرِ مشترک دارند. |
| مستندات در مخزن | `docs/` + همین سند + Swagger + توضیحِ «چرا» کنارِ کدِ حساس. |
@@ -929,25 +943,26 @@ fetch ← `npm ci` ← بررسیِ نوع‌ها ← build در `dist.next` ←
### ۱۴-۳ مقیاسِ اجرا: کجا گلوگاه است و بعد چه
وضعِ فعلی: ۳ worker × ۳۲ نخ = ۹۶ درخواستِ هم‌زمان؛ درخواستِ عادی چند ده میلی‌ثانیه، جوابِ دستیار روی سرور میانه‌ی ۵ تا ۸ ثانیه و گاهی ۴۰ ثانیه، که تمامش یک نخ را نگه می‌دارد. در لودتست تا ۷۵ پرسشِ هم‌زمان صفحه‌های عادی صف نکشیدند و از ۱۰۰ به بعد کشیدند. توکن stateless است، پس هر تعداد نسخه‌ی دیگر از بک‌اند پشتِ nginx بی‌تغییرِ کد کار می‌کند — به شرطِ دیتابیس و کشِ مشترک.
وضعِ فعلی: ۴ worker × ۳۲ نخ؛ هر worker حداکثر ۸ اتصالِ Postgres از pool قرض می‌گیرد و حداکثر ۱۲ نخ به دستیار می‌دهد، تا رگبارِ سوال نقشه و آزمون را نخواباند. توکن stateless است، پس هر تعداد نسخه‌ی دیگر از بک‌اند پشتِ nginx بی‌تغییرِ کد کار می‌کند.
**آزمونِ بار روی خودِ سرور** (بازیکنانِ شبیه‌سازی‌شده که هر ۱ تا ۵ ثانیه یک درخواست می‌زنند): ۳۰۰ نفر ۸۸ درخواست در ثانیه با میانه‌ی ۰٫۱ ثانیه؛ ۶۰۰ نفر ۱۳۷ درخواست با میانه‌ی ۱٫۴ ثانیه و بی‌خطا؛ ۱٬۰۰۰ نفر به سقفِ ~۱۳۵ درخواست در ثانیه رسید (صدکِ ۹۵ ۱۵ ثانیه). گلوگاه CPUِ ماشینِ مشترک است، نه دیتابیس: دو سرویسِ خرابِ تیم‌های دیگر مدام ری‌استارت می‌شوند و حدودِ ۳ از ۸ هسته را می‌گیرند.
| # | گلوگاه | راه‌حلِ ساده‌ی بعدی | نیاز |
|---|---|---|---|
| ۱ | SQLite فقط یک نویسنده دارد | Postgres (همین حالا روی سرور هست)؛ تغییرِ تنظیمات + انتقالِ داده؛ حالتِ `IMMEDIATE` برداشته می‌شود و قفلِ ردیف واقعی می‌شود. | ادمینِ سرور |
| ۲ | کش و سقفِ نرخِ جدا برای هر worker | Redisِ اختصاصیِ خودمان، بعد `CACHE_URL`. Redisِ موجودِ ماشین مالِ دیگران است و وصل نمی‌شویم. | تصمیمِ باز + ساختِ Redis |
| ۳ | جوابِ دستیار نخ را نگه می‌دارد | صفِ کار (مثلاً Celery، که RabbitMQ و Redis‌اش روی سرور هست) یا ویوی async برای `/api/ai/ask`. | کد |
| ۴ | یک سرور برای همه | چند نسخه‌ی gunicorn پشتِ nginx؛ فرانت از CDNِ آروان. | زیرساخت |
| ۵ | هزینه‌ی دستیار با کاربرِ زیاد | کش، سقفِ روزانه (هست)، و سنجشِ مدلِ ارزان‌تر روی همان ابزارِ `rag_model_benchmark`. | سنجش |
| ۱ | CPUِ ماشینِ مشترک | رفعِ دو سرویسِ خرابِ دیگران، هسته‌ی بیشتر و ۸ worker (برآورد ~۲۷۰ درخواست در ثانیه) | ادمینِ سرور |
| ۲ | جوابِ دستیار نخ را نگه می‌دارد | صفِ کار یا ویوی async برای `/api/ai/ask` | کد |
| ۳ | یک سرور برای همه | چند نسخه‌ی gunicorn پشتِ nginx یا سرورِ دوم؛ فرانت از CDNِ آروان | زیرساخت |
| ۴ | هزینه‌ی دستیار با کاربرِ زیاد | کش، سقفِ روزانه (هست)، و سنجشِ مدلِ ارزان‌تر با `rag_model_benchmark` | سنجش |
بهینه‌سازی‌های انجام‌شده: رفعِ N+1 در صفحه‌ی اصلی (از ۱+N کوئری به ۳ کوئریِ ثابت)، کشِ نقشه، کشِ جدولِ رتبه (۶۰ ثانیه)، کشِ جوابِ دستیار، ادغامِ چند درخواست در یکی ( `/api/home`، `/api/user/summary`، `/api/nodes/{id}/quiz` )، و حجمِ بارِ اولِ فرانت از ۵۰۵ به ۸۵ کیلوبایت.
بهینه‌سازی‌های انجام‌شده: رفعِ N+1 در صفحه‌ی اصلی، کشِ نقشه و جدولِ رتبه، کشِ جوابِ دستیار، poolِ اتصال (پیش از آن ۱۰۰ کاربرِ هم‌زمان ۲۱٪ خطای ۵۰۰ می‌گرفتند؛ بعد صفر)، ادغامِ چند درخواست در یکی ( `/api/home`، `/api/user/summary`، `/api/nodes/{id}/quiz` )، و حجمِ بارِ اولِ فرانت از ۵۰۵ به ۸۵ کیلوبایت.
## ۱۵. تست و کیفیت
| چه | چطور |
|---|---|
| تستِ خودکارِ بک‌اند | **۷۷۹ تست**، همه سبز ( `python manage.py test` ). روی فایلِ دیتابیسِ واقعی ( `.test-db.sqlite3` ) اجرا می‌شوند تا رفتارِ قفل‌ها مثلِ سرور باشد. هر باگِ پیداشده در بازبینی‌ها یک تستِ اختصاصی دارد ( `tests_audit_fixes.py`، `tests_otp_limits.py`، `tests_hearts_gate.py`، …). هم‌زمانی با دو thread واقعی آزموده می‌شود (اعتبارِ دستیار، آخرین قلب). |
| تستِ خودکارِ بک‌اند | **۹۷۳ تست**، همه سبز ( `python manage.py test --noinput` ). روی فایلِ دیتابیس ( `.test-db.sqlite3` ) اجرا می‌شوند و روی Postgres هم سبزند. هر باگِ پیداشده در بازبینی‌ها یک تستِ اختصاصی دارد ( `tests_audit_fixes.py`، `tests_otp_limits.py`، `tests_hearts_gate.py`، …). هم‌زمانی با دو thread واقعی آزموده می‌شود (اعتبارِ دستیار، آخرین قلب). |
| قرارداد با فرانت | `content/tests_frontend_contract.py` و build با TypeScriptِ strict در فرانت. |
| تستِ روی سایتِ زنده | هر صفحه با حساب‌های نمایشی روی سرورِ واقعی؛ مرورگرِ خودکار (puppeteer) برای وارسیِ صفحه‌ها. |
| تستِ روی سایتِ زنده | با حساب‌های آزمونگر روی سرورِ واقعی؛ مرورگرِ خودکار (puppeteer) برای وارسیِ صفحه‌ها؛ آزمونِ بار با `tools/loadtest/`. |
| کیفیتِ دستیار | `rag_model_benchmark` (مسئله‌های واقعیِ کتاب از مسیرِ واقعی، با هزینه‌ی واقعی)، `ai_feedback_report` (👍/👎 به تفکیکِ نسخه‌ی پرامپت)، وارسیِ چشمیِ نمونه پیش از اعتماد به هر اجرای انبوه. |
| بی‌هزینه آزمودن | `tools/fake_openai.py` جای GapGPT می‌نشیند. |
| پایش | لاگِ JSONِ هر درخواست + `/api/health` ؛ محلی با Grafana/Loki ( `docs/docker.md` ). |
@@ -956,40 +971,40 @@ fetch ← `npm ci` ← بررسیِ نوع‌ها ← build در `dist.next` ←
## ۱۶. وضعیتِ فعلی، محدودیت‌ها و تصمیم‌های باز
### وضعیت در تاریخِ نگارشِ سند (۱۰ مهر ۱۴۰۵، روی سرورِ زنده)
### وضعیت در تاریخِ نگارشِ سند (۲۲ مهر ۱۴۰۵، روی سرورِ زنده)
| بخش | وضعیت | توضیح |
|---|---|---|
| اپ و API | زنده | هر دو روی آخرین نسخه؛ همه‌ی صفحه‌ها با حساب‌های نمایشی وارسی شد |
| محتوا | کامل | ۸ پایه؛ هیچ جزیره‌ای زیرِ ۴ سؤال نیست |
| پیامکِ ورود | کار می‌کند | کدِ واقعی به گوشی رسید (الگوی SMSVerify)؛ هر پیامک هزینه دارد |
| دستیار + شکل | کار می‌کند | کُند (~۳۰ ثانیه، از سمتِ GapGPT)؛ ~یک شکل از پنج ایراد دارد |
| اپ و API | زنده | با حساب‌های آزمونگر وارسی می‌شود؛ هنوز به روی کاربرِ عمومی باز نشده |
| محتوا | کامل برای سوم تا دوازدهم | ۱۵ کتاب؛ همه‌ی جزیره‌ها سؤال دارند؛ جزیره‌های تمرینِ آخرِ هر فصل |
| پیامکِ ورود | کار می‌کند | الگوی SMSVerify؛ هر پیامک هزینه دارد؛ سقفِ روزانه‌ی کلِ سامانه ۲٬۰۰۰ |
| دستیار + شکل | کار می‌کند | فقط ریاضی؛ ~یک شکل از پنج ایراد دارد |
| زرین‌پال | نیمه | درخواستِ پرداخت پذیرفته می‌شود؛ پرداختِ واقعی تا انتها آزموده نشده |
| کافه‌بازار | منتظرِ اپ | اتصالِ API برقرار؛ سه اشتراک در پیشخان (۱۰۳۴۲–۱۰۳۴۴) و روی پلن‌ها؛ منتظرِ ساختِ اپِ اندروید و یک خریدِ واقعی |
| اشتراک و قلب | زنده | سه اشتراک روی سایت؛ قلبِ صفر بازی را متوقف می‌کند |
| پیامِ گروهی | پایه | ذخیره و هدف‌گیری هست؛ زمان‌بندیِ خودکار نه |
| بکاپِ خودکار | ندارد | دستی پیش از هر انتشار |
| کافه‌بازار | منتظرِ APKِ تازه | سه اشتراک (۱۰۳۴۲–۱۰۳۴۴) روی پلن‌ها؛ کدِ اپ آماده ولی build و خریدِ واقعی انجام نشده |
| اشتراک و قلب | زنده | فصلِ اولِ هر کتاب رایگان؛ قلبِ صفر بازی را متوقف می‌کند |
| بکاپ | خودکار | کپیِ زنده روی سرورِ دوم، بازیابی آزموده شده |
| ظرفیت | سنجیده | ۶۰۰ بازیکنِ هم‌زمان بی‌خطا؛ سقف CPUِ ماشینِ مشترک |
| پیامِ گروهی | پایه | ذخیره و هدف‌گیری هست؛ ارسالِ خودکار نه |
### تصمیم‌هایی که با مدیریت است، نه با کد
| موضوع | چرا تصمیم لازم است |
|---|---|
| عوض کردنِ کلیدِ امضا | کد آماده؛ همه یک بار از حساب بیرون می‌افتند — زمانش را باید تعیین کرد. |
| پاک‌سازیِ داده‌ی دمو | حساب‌های آزمایشیِ شماره‌دار ۱۰ مهر پاک شدند (با بکاپ)؛ ۷ شماره‌ی نمایشی و حساب‌هایشان عمداً مانده‌اند. |
| مانع در برابرِ پر کردنِ سقفِ پیامک | CAPTCHA تجربه‌ی ورود را سخت‌تر می‌کند؛ بی‌آن یک مهاجم ورود را برای همه می‌بندد. |
| سقفِ دستیار و سهمیه‌ی رایگان | بعد از دو هفته مصرفِ واقعی بازبینی شود؛ سود در `docs/ai-cost-and-guardrails.md` حساب شده. |
| یک فصلِ رایگان برای هر کتاب | تعبیرِ ما از «یک فصل رایگان» برای پایه‌های چندکتابی؛ تأییدِ نهایی با مدیریت. |
| سیاستِ بازپرداخت | الان روزهای اشتراکِ همان سفارش پس گرفته می‌شود؛ پول از پنلِ درگاه. |
| Redis و Postgres | هماهنگی با ادمینِ سرور؛ هر دو روی همان ماشین موجودند. |
| پورتِ ۸۰ و دسترسیِ SSH | کارِ ادمینِ سرور. |
| پرداختِ درگاه داخلِ اپِ بازار | بازار پرداختِ محتوای دیجیتال بیرون از پرداختِ خودش را محدود می‌کند؛ پیش از انتشار روشن شود. |
| CPU و دسترسیِ SSH | کارِ ادمینِ سرور. |
| پیشرفت با هر نمره | در حالِ حاضر جزیره با هر نمره‌ای تمام می‌شود؛ اگر حدِ قبولی لازم است، یک تصمیمِ آموزشی است. |
### کارِ بعدی، به ترتیبِ ارزش
1. بستنِ موارد ۱ تا ۳ِ امنیت و پاک‌سازیِ داده‌ی دمو.
2. بکاپِ خودکارِ دیتابیس، بیرون از همان سرور.
3. یک پرداختِ واقعیِ کامل با زرین‌پال.
4. Redisِ اختصاصی (`CACHE_URL`) و بعد Postgres.
1. یک پرداختِ واقعیِ کامل با زرین‌پال، و ساختِ APKِ تازه و یک خریدِ واقعی از کافه‌بازار.
2. بستنِ مواردِ باز در `docs/security-before-production.md` (پیامک، پنلِ ادمین، دسترسیِ سرور).
3. بازبینیِ سؤال‌های جزیره‌های تمرین با چشمِ معلم، به کمکِ رأی و گزارشِ دانش‌آموزان.
4. رفعِ گلوگاهِ CPU با ادمینِ سرور پیش از هر لانچِ بزرگ.
5. صفِ کار برای دستیار؛ سنجشِ شکل روی نمونه‌ی بزرگ‌تر.
6. تعریفِ سه بسته در پنلِ کافه‌بازار و اپِ اندرویدی با Poolakey.
## پیوست
@@ -1020,7 +1035,7 @@ fetch ← `npm ci` ← بررسیِ نوع‌ها ← build در `dist.next` ←
python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt
cp .env.example .env # DJANGO_DEBUG=1 برای محلی
python manage.py migrate && python manage.py seed_content && python manage.py runserver
python manage.py test # کلِ تست‌ها
python manage.py test --noinput # کلِ تست‌ها
python manage.py spectacular --file schema.yaml # قراردادِ API
# محتوا و دستیار
@@ -1045,12 +1060,14 @@ npm run build # type-check + build در dist/
| `README.md` (هر دو مخزن) | درِ ورودی و راه‌اندازی |
| `docs/architecture.md` | تصمیم‌های معماری و دلیلشان، با جزئیات |
| `docs/operations.md` | انتشار، متغیرها، بکاپ، جابه‌جاییِ دامنه، ورودِ تیم |
| `docs/security-before-production.md` | ۱۸ موردِ امنیتیِ باز با خطر و راهِ بستن |
| `docs/security-before-production.md` | مواردِ امنیتیِ باز با خطر و راهِ بستن، و حفاظ‌های موجود |
| `docs/content-authoring.md` | ویرایشِ محتوا و ساختِ انبوهِ سؤال |
| `docs/extraction-playbook.md` | خواندنِ کتاب با مدلِ بینایی: هزینه و درس‌ها |
| `docs/ai-cost-and-guardrails.md` | هزینه‌ی هر سوالِ دستیار، سودِ هر اشتراک و قاعده‌ی خرج، محافظِ «فقط ریاضی» |
| `docs/model-prices.md` | دفترِ قیمتِ اندازه‌گیری‌شده‌ی مدل‌ها، با تاریخ؛ اگر قیمت عوض شد ردیف اضافه شود |
| `data/textbooks/README.md` | افزودنِ کتابِ تازه |
| `tools/loadtest/README.md` | آزمونِ بار، روی لپ‌تاپ و روی سرورِ مشترک |
| `CLAUDE.md` | راهنمای agentهای کدنویس و قواعدِ کار روی سرورِ مشترک |
| `/api/docs/` | قراردادِ زنده و قابلِ امتحانِ API |
تدریسینو · مستندِ فنی · علیرضا زارعیان · مهرِ ۱۴۰۵ <br> عددها از کد، اسکیمای API، دیتابیس و سنجش‌های ثبت‌شده در مخزن استخراج شده‌اند.