Content ------- Every node in grades 8, 9, 10, 11 and 12 now holds a full four-question quiz — 121 more questions, filling the two حسابان دوازدهم nodes in فصل ۵: کاربردهای مشتق that were empty and the two ریاضی هشتم nodes that were short. The repo's own seed guard was already failing on those two empty nodes; it passes now. Grades 3, 6 and 7 are the other team's books and were left alone. 39 of their 145 nodes still hold fewer than four questions, but none is empty, so every node on the map still plays. An audit over all 1,593 questions in grades 8–12 found no structural fault: no question with the wrong number of options, no question with zero or two correct answers, no empty or duplicated option, no leftover LaTeX, and no two identical questions inside one lesson. Contract -------- Three endpoints were missing or malformed in the OpenAPI schema, which matters more than it looks: that schema is the contract the frontend generates its client from, so a view missing there is a view the other repo cannot call. - NotificationMarkReadView was dropped from the schema entirely, because spectacular could not guess a request body for its PATCH and gives up on the whole view rather than the one field. It declares request=None. - PaymentRedirectView was dropped for the same reason; it answers with a 302 and no JSON body, which it now says. - /api/health and /api/health/ collided on operationId and were emitted as health_retrieve and health_retrieve_2. The slash variant exists only for a monitor that calls it that way, so it is excluded from the schema. `spectacular` now runs clean — no errors, no warnings. Tidying ------- Condensed the two longest comment blocks (18 and 14 lines) in rag_verify_solutions and settings, keeping the measurement and the rule and dropping the retelling. README gains the bulk question-authoring workflow; the playbook's subagent section now points at the real tool instead of describing the steps by hand. 458 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
296 lines
18 KiB
Markdown
296 lines
18 KiB
Markdown
# Tadrisino API
|
||
|
||
بکاند اپلیکیشن آموزشی Tadrisino — Django + Django REST Framework.
|
||
|
||
احراز هویت با OTP، محتوای درسی و پیشرفت کاربر، دستیار هوش مصنوعی، اشتراک و پرداخت،
|
||
تخفیف، اعلانها، همکاری در فروش، پشتیبانی و حساب خانواده.
|
||
|
||
| اپ | مسئولیت |
|
||
|---|---|
|
||
| `accounts` | احراز هویت، session، پروفایل کاربر |
|
||
| `content` | پایهها، مباحث، درسها، سوالها، پیشرفت |
|
||
| `progress` | آمار، لیدربرد، streak |
|
||
| `ai_tutor` | مکالمات دستیار هوش مصنوعی |
|
||
| `rag` | لایهی RAG: بازیابی از Qdrant + تولید با GPT، پشتِ `POST /api/ai/ask` |
|
||
| `subscription` / `payment` / `discount` | پلن، سفارش، تراکنش، کد تخفیف |
|
||
| `notifications`, `affiliate`, `support`, `family` | امکانات جانبی |
|
||
| `statuslog` | لاگ عمومی تغییر status، مستقل از بقیهی اپها |
|
||
|
||
---
|
||
|
||
## ۱. راهاندازی محیط توسعه
|
||
|
||
### پیشنیازها
|
||
- پایتون ۳.۱۲
|
||
- pip
|
||
|
||
### تنظیم محیط
|
||
```bash
|
||
python3 -m venv venv
|
||
source venv/bin/activate # ویندوز: venv\Scripts\activate
|
||
pip install -r requirements.txt
|
||
cp .env.example .env
|
||
```
|
||
|
||
`.env.example` (تمام متغیرهایی که واقعاً توسط اپ خونده میشن):
|
||
```env
|
||
DJANGO_DEBUG=1
|
||
DJANGO_SECRET_KEY=
|
||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||
CACHE_URL=
|
||
KAVENEGAR_API_KEY=
|
||
GRAFANA_ADMIN_PASSWORD=admin
|
||
```
|
||
مقادیر `.env` رو به شل خودتون export کنید (یا از `direnv`/`python-dotenv` استفاده کنید):
|
||
```bash
|
||
export DJANGO_DEBUG=1 # ویندوز (PowerShell): $env:DJANGO_DEBUG=1
|
||
```
|
||
> `DEBUG` دیفالتش `False` است. برای فعالکردنِ کمککنندههای dev-only در Swagger
|
||
> (`DEV_MASTER_OTP` و فیلدِ `devOtp` توی پاسخها)، محلی `DJANGO_DEBUG=1` رو ست کنید.
|
||
|
||
### سریعترین راه اجرای پروژه
|
||
```bash
|
||
python manage.py migrate
|
||
python manage.py runserver
|
||
```
|
||
اپ روی `http://127.0.0.1:8000` بالا میاد.
|
||
|
||
### مایگریشنها
|
||
```bash
|
||
python manage.py migrate
|
||
```
|
||
|
||
### دیتای تستی (Seed)
|
||
یک دیتاست کوچک و idempotent میسازه که همهی اپها رو پوشش میده (کاربر، محتوا، پیشرفت،
|
||
اشتراک، پرداخت، تخفیف، دستیار هوش مصنوعی، اعلان، افیلیت، پشتیبانی، خانواده). اجرای مکرر
|
||
امن است — با `get_or_create` هیچ رکورد تکراریای نمیسازه. پیشفرض فقط وقتی `DEBUG=True`
|
||
اجرا میشه، مگر با `--force`.
|
||
|
||
```bash
|
||
python manage.py seed_test_data # لوکال (DEBUG=True)
|
||
python manage.py seed_test_data --force # staging (DEBUG=False)
|
||
python manage.py seed_test_data --reset # پاککردنِ دیتای قبلیِ seed و ساختِ دوباره
|
||
```
|
||
|
||
### مستندات API
|
||
| | |
|
||
|---|---|
|
||
| Swagger UI | `http://127.0.0.1:8000/api/docs/` |
|
||
| Redoc | `http://127.0.0.1:8000/api/redoc/` |
|
||
| OpenAPI schema | `http://127.0.0.1:8000/api/schema/` |
|
||
| پنل ادمین | `http://127.0.0.1:8000/admin/` |
|
||
|
||
**شروع سریع در Swagger:**
|
||
1. `POST /api/auth/session` با `{"type":"guest","deviceId":"d_test1"}` → یک `token` بگیرید.
|
||
2. روی دکمهی **Authorize** بزنید و فقط خودِ `token` رو (بدون کلمهی Bearer) وارد کنید.
|
||
3. برای تست لاگین با شماره: `POST /api/auth/otp/request` بزنید، یا در محیط توسعه کد **0000**
|
||
رو با هر شمارهای در `POST /api/auth/login` امتحان کنید (dev-only، نیاز به `DEBUG=True`).
|
||
|
||
### دستورهای پرکاربرد
|
||
```bash
|
||
python manage.py createsuperuser # اختیاری، برای /admin
|
||
python manage.py test # اجرای کل تستسوییت
|
||
```
|
||
|
||
### دستیار هوش مصنوعی (RAG)
|
||
|
||
`POST /api/ai/ask` پاسخ را از روی محتوای واقعیِ کتاب میسازد: embedding سوال →
|
||
جستجوی Qdrant محدود به همان درس/فصل → تولید با GPT. تیکههای استفادهشده در
|
||
`usedChunks` برمیگردند.
|
||
|
||
- نیازمندِ `OPENAI_API_KEY` و `QDRANT_API_KEY` در `.env` (اپ `rag`؛ تنظیماتِ
|
||
`OPENAI_*` / `RAG_*` در `config/settings.py`). بدونِ کلید، همان پاسخِ stub قبلی.
|
||
- تست در Swagger: `POST /api/ai/ask` با
|
||
`{"userQuestion":"...","subject":"ریاضی","grade":"ششم","lessonNumber":2}`.
|
||
- `"stream": true` پاسخ را بهصورت SSE توکنبهتوکن میفرستد (قراردادِ JSON بدونِ این فلگ دستنخورده).
|
||
- `python manage.py rag_catalog` فهرستِ زندهی collectionهای Qdrant را نشان میدهد (فقطخواندنی).
|
||
- افزودنِ محتوای خودمان: `POST /api/rag/ingest/text` و `/api/rag/ingest/pdf` (داخلی، هدرِ `X-Internal-Key`).
|
||
|
||
---
|
||
|
||
## ۲. راهنمای دیپلوی پروداکشن
|
||
|
||
### بالا آوردنِ نسخهی جدید — یک دستور
|
||
|
||
```bash
|
||
./deploy.sh
|
||
```
|
||
|
||
همین. اسکریپت به ترتیب کدِ تازه را میگیرد، وابستگیها را نصب میکند، مهاجرتها
|
||
را اعمال میکند، فایلهای استاتیک را جمع میکند، سرویس را ریاستارت میکند، و در
|
||
پایان **خودش بررسی میکند که واقعاً بالا آمده یا نه**. اگر بالا نیامد، علتهای
|
||
رایج را چاپ میکند.
|
||
|
||
چرا این مهم است: قبلاً اینها چند کارِ دستی به ترتیبِ درست بودند و فراموش شدنِ
|
||
هر کدام اپ را میخواباند، با خطایی که گویا نبود. مثلاً اضافه شدنِ یک پکیجِ جدید
|
||
به `requirements.txt` بدونِ `pip install` باعثِ `ModuleNotFoundError` موقعِ بالا
|
||
آمدن میشود و کاربر فقط `502 Bad Gateway` میبیند. اسکریپت همیشه `pip install`
|
||
را اجرا میکند تا این حالت اصلاً پیش نیاید.
|
||
|
||
بارِ اول، نامِ سرویس را بدهید تا دفعههای بعد خودکار ریاستارت شود:
|
||
|
||
```bash
|
||
TADRISINO_SERVICE=<نامِ سرویس> ./deploy.sh
|
||
```
|
||
|
||
متغیرهای اختیاری: `SKIP_PULL=1` (بدونِ git pull)، `TADRISINO_HEALTH_URL`،
|
||
`PIP_INDEX_URL` (اگر pypi.org تایماوت میدهد).
|
||
|
||
### بررسیِ سلامت از بیرون
|
||
|
||
```bash
|
||
curl https://<host>/api/health
|
||
```
|
||
|
||
بدونِ احرازِ هویت و بدونِ SSH میگوید دیتابیس سالم است یا نه، مهاجرتِ
|
||
اعمالنشده مانده یا نه، کش کار میکند یا نه، و چند پایه/درس/سوال روی آن سرور
|
||
هست. `200` یعنی سالم، `503` یعنی چیزی واقعاً خراب است. هیچ سکرتی برنمیگرداند.
|
||
|
||
این همان چیزیست که جلوی «کد رفته ولی مهاجرت اجرا نشده» را میگیرد — حالتی که
|
||
اپ ظاهراً بالا میآید و بعد زیرِ بارِ واقعی میشکند.
|
||
|
||
### مدیریتِ محتوا بدونِ ترمینال
|
||
|
||
هیچ محتوایی لازم نیست با دستور روی سرور گذاشته شود.
|
||
|
||
| کار | کجا |
|
||
|---|---|
|
||
| افزودن/ویرایشِ **یک** سوال | `/admin/content/question/` → «افزودن» (انتخابِ درس با جستجو) |
|
||
| وارد کردنِ **فلهای** سوال | همان صفحه → «وارد کردنِ سوالها از فایل» |
|
||
| گرفتنِ خروجیِ سوالها | چند سوال را انتخاب کنید → اکشنِ «دریافتِ فایل» |
|
||
| وارد کردنِ **برنامهی درسی** (پایه/فصل/درس) | `/admin/content/grade/` → «وارد کردنِ برنامهی درسی از فایل» |
|
||
| گرفتنِ خروجیِ برنامهی درسی | پایهها را انتخاب کنید → اکشنِ «دریافتِ فایلِ برنامهی درسی» |
|
||
|
||
خروجیِ هر کدام دقیقاً همان قالبیست که ورودیاش میپذیرد، پس چرخهی
|
||
«بگیر → ویرایش کن → برگردان» بسته است و برای گرفتنِ نمونه لازم نیست کسی
|
||
مستندات بخواند.
|
||
|
||
دو تضمین:
|
||
- **سوالها:** فایلِ نامعتبر هیچ چیزی نمینویسد — یا همه وارد میشوند یا هیچکدام.
|
||
- **برنامهی درسی:** چیزی پاک نمیشود. هر چه در فایل هست ساخته/بهروز میشود و
|
||
هر چه نیست دستنخورده میماند، پس یک فایلِ ناقص محتوای سرور را از بین نمیبرد.
|
||
|
||
### ساختِ انبوهِ سوال از کتاب
|
||
|
||
وقتی کتابی تازه روی Qdrant آمده و باید برایش سوال ساخته شود، مسیرِ رایگان این است:
|
||
|
||
```bash
|
||
# اسکلتِ برنامهی درسی از فهرستِ خودِ کتاب (بدونِ هزینه)
|
||
python manage.py seed_curriculum_from_textbook --subject ریاضی --grade نهم --dry-run
|
||
python manage.py seed_curriculum_from_textbook --subject ریاضی --grade نهم
|
||
|
||
# جفتهای (درس، مسئلهی تأییدشده) را برای سوالنویسی بیرون بده (بدونِ هزینه)
|
||
python manage.py generate_questions_from_chunks --subject ریاضی --grade نهم --export /tmp/batches
|
||
```
|
||
|
||
خروجی چند فایلِ JSON است بهعلاوهی یک `*-RULES.txt` که قواعدِ ساختِ سوال را
|
||
دارد. سوالها را با آن قواعد بنویسید و از «وارد کردنِ فلهای سوال» در جدولِ
|
||
بالا واردشان کنید. همان فرمان بدونِ `--export` کار را به مدلِ پولی میسپارد،
|
||
ولی گرانتر است و کیفیتِ بهتری نمیدهد. جزئیات و هزینههای اندازهگیریشده در
|
||
`docs/extraction-playbook.md`.
|
||
|
||
`--dry-run` روی هر دو فرمان کار میکند و پیش از هر نوشتنی نشان میدهد چه ساخته
|
||
خواهد شد.
|
||
|
||
### دادهی تستی روی سرور
|
||
|
||
`seed_test_data` فقط برای محیطِ لوکال است و **نباید** روی سروری که کاربرِ واقعی
|
||
دارد اجرا شود؛ ردیفهایی با پیشوندِ `[SEED]`/`[DEMO]` میسازد که در API دیده
|
||
میشوند. محتوای واقعی از مسیرِ جدولِ بالا میرود، نه از این دستور.
|
||
|
||
|
||
### متغیرهای محیطی موردنیاز
|
||
| متغیر | لازم در پروداکشن | توضیح |
|
||
|---|---|---|
|
||
| `DJANGO_DEBUG` | باید ستنشده یا `0` باشه | دیفالتهای امنیتیِ پروداکشن رو فعال میکنه |
|
||
| `DJANGO_SECRET_KEY` | **بله** | بدونش وقتی `DEBUG=False` است اپ اصلاً بالا نمیاد |
|
||
| `DJANGO_ALLOWED_HOSTS` | **بله** | لیستِ host ها، کاما-جدا |
|
||
| `CACHE_URL` | **بله** | آدرسِ Redis، مثلاً `redis://host:6379/0` — برای rate limitِ مشترک وقتی `DEBUG=False` لازمه |
|
||
| `KAVENEGAR_API_KEY` | **بله** | ارسال واقعیِ OTP از طریق کاوهنگار |
|
||
| `GRAFANA_ADMIN_PASSWORD` | فقط اگه از استکِ Grafana استفاده میکنید | بخشِ Docker پایین رو ببینید |
|
||
|
||
### تنظیمات پروداکشن
|
||
- `DEBUG` باید ست **نشه** (یا `0` باشه). با `DEBUG=False`:
|
||
- `SECRET_KEY` اجباریه — هیچ fallback ناامنی وجود نداره.
|
||
- `CACHE_URL` (Redis) اجباریه — `LocMemCache` فقط برای dev هست.
|
||
- `DEV_MASTER_OTP` و فیلدِ `devOtp` توی پاسخها غیرفعال میشن.
|
||
- برای ترافیک واقعی بهجای SQLیت پیشفرض، از یک دیتابیسِ پروداکشنگرید استفاده کنید
|
||
(پروژه با همون SQLite شیپ میشه؛ عوضکردنِ engine یک تصمیمِ زیرساختیه، خارج از این ریپو).
|
||
- از طریق یک سرورِ WSGI/ASGI (مثلاً gunicorn) جلوی اپ سرو کنید — تنظیماتِ Docker موجود
|
||
همچنان `runserver` اجرا میکنه و فقط برای dev/local مستند شده (پایین رو ببینید).
|
||
|
||
### تنظیمات Docker/دیپلوی
|
||
`docker-compose.yml` موجود، اپ رو **بههمراه** یک استکِ لاگِ متمرکز
|
||
(Loki + Grafana Alloy + Grafana) اجرا میکنه. این یک تنظیمِ **dev/local** هست، نه دیپلویِ
|
||
پروداکشن (سرویسِ `django` همچنان `runserver` اجرا میکنه، نه یک سرورِ پروداکشن).
|
||
|
||
```bash
|
||
cp .env.example .env # اختیاری — بدونش compose از دیفالتهای dev استفاده میکنه
|
||
docker compose up --build
|
||
```
|
||
|
||
| سرویس | آدرس | توضیح |
|
||
|---|---|---|
|
||
| اپ Django | `http://localhost:8000` | |
|
||
| Grafana | `http://localhost:3000` | دسترسیِ anonymous admin، فقط محیطِ محلی |
|
||
| Loki | `http://localhost:3100` | ذخیرهسازیِ لاگ، در معرضِ کاربر نیست |
|
||
| Alloy | `http://localhost:12345` | رابطِ دیباگِ اختیاری |
|
||
|
||
### ترتیبِ لازمِ بالا آمدن
|
||
Compose خودش این رو مدیریت میکنه:
|
||
1. اول `loki` بالا میاد.
|
||
2. `alloy` و `grafana` بعد از `loki` بالا میان (`depends_on`).
|
||
3. `django` هیچ وابستگیای به استکِ لاگ نداره — خودش `migrate` بعد `runserver` رو
|
||
اجرا میکنه و منتظرِ Loki/Alloy/Grafana نمیمونه.
|
||
|
||
### دستورهای manage.py داخلِ Docker
|
||
اگه فقط از طریقِ Docker اجرا میکنید (بدونِ venv محلی)، `python manage.py ...` رو مستقیم
|
||
روی سیستم خودتون اجرا نکنید — باید داخلِ کانتینرِ `django` اجرا بشه:
|
||
```bash
|
||
docker compose exec django python manage.py createsuperuser
|
||
docker compose exec django python manage.py seed_test_data
|
||
docker compose exec django python manage.py test
|
||
```
|
||
(`migrate` نیازی به این نیست — طبقِ بالا، خودکار قبل از `runserver` اجرا میشه.)
|
||
|
||
### مایگریشنها
|
||
```bash
|
||
python manage.py migrate --noinput
|
||
```
|
||
در Docker، این بهصورت خودکار بهعنوان بخشی از دستورِ سرویسِ `django`، قبل از `runserver` اجرا میشه.
|
||
|
||
### راهنمای seed برای staging/تست
|
||
- هرگز `seed_test_data` رو روی یک دیتابیسِ پروداکشنِ واقعی اجرا نکنید.
|
||
- روی staging (`DEBUG=False`)، صریحاً با `--force` اجرا کنید:
|
||
```bash
|
||
python manage.py seed_test_data --force
|
||
```
|
||
- برای پاککردن و ساختِ دوبارهی دیتای seed بین چرخههای تست، از `--reset` استفاده کنید.
|
||
همهی رکوردهای seedشده در فیلدهای متنیِ قابلمشاهده با تگِ `[SEED]` مشخص شدن، برای
|
||
شناساییِ راحتتر و پاکسازی.
|
||
|
||
### چطور API دیپلویشده رو verify کنیم
|
||
```bash
|
||
curl -i https://<host>/api/schema/ # باید 200 برگردونه (OpenAPI schema)
|
||
curl -i https://<host>/api/docs/ # باید 200 برگردونه (Swagger UI)
|
||
curl -i -X POST https://<host>/api/auth/session \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"type":"guest","deviceId":"verify-1"}' # باید یک token برگردونه
|
||
```
|
||
|
||
### نکات مهمِ پروداکشن
|
||
- `DEV_MASTER_OTP` (کد `0000`) و فیلدِ `devOtp` توی پاسخها فقط وقتی `DEBUG=True` وجود
|
||
دارن — قبل از رفتن روی محیطِ واقعی مطمئن بشید `DEBUG` خاموشه.
|
||
- احراز هویت با Bearer token امضاشده و stateless (`django.core.signing`)، هماهنگ با مدل
|
||
`Session` — بدون کتابخانهی JWT خارجی.
|
||
- `accounts.User` یک مدل بیزنسیِ مستقله، **نه** `AUTH_USER_MODEL` جنگو.
|
||
- همهی لاگهای ساختاریافتهی درخواست (`method`/`path`/`status`/`duration`/`user`، بدونِ
|
||
بدنه/هدر/سکرت) روی stdout و — وقتی `LOG_DIR` ست باشه (مثلِ داخلِ Docker) — در
|
||
`/var/log/django/app.log` نوشته میشن؛ قابلِ مشاهده در Grafana از طریقِ استکِ بالا.
|
||
- `POST /api/ai/ask` با تنظیمِ `OPENAI_API_KEY`/`QDRANT_API_KEY` پاسخِ واقعیِ RAG میدهد
|
||
(اپ `rag`)؛ بدونِ کلید همان stub. جزئیات: بخشِ «دستیار هوش مصنوعی (RAG)» بالاتر.
|
||
- ارسالِ OTP از طریقِ کاوهنگار (`KAVENEGAR_API_KEY`) انجام میشه؛ شکستِ ارسال بهجای
|
||
موفقیتِ ساختگی، خطای صریحِ `503` برمیگردونه.
|
||
- هر خطای پیشبینینشده (نه فقط خطاهای شناختهشدهی API) همیشه پاسخِ JSON برمیگردونه،
|
||
نه صفحهی HTML پیشفرضِ جنگو، و کامل با traceback لاگ میشه (`config/exceptions.py`).
|