سوالها از قبل فلهای وارد میشدند ولی دو شکاف مانده بود: افزودنِ تکی عملاً غیرقابلِ استفاده بود، و خودِ برنامهی درسی هیچ راهی جز ترمینالِ سرور نداشت. افزودنِ تکیِ سوال: انتخابِ درس یک دراپداونِ ۱۳۰۷تایی بود. حالا autocomplete است و بر اساسِ عنوانِ درس، فصل و پایه جستجو میشود. برنامهی درسی (Grade → Unit → Node → Lesson) حالا از /admin/content/grade/ وارد و خارج میشود. تا امروز رساندنش به سرور یعنی اجرای rag_import_collection و بعد seed_curriculum_from_textbook روی همان سرور — یعنی وابستگی به Qdrant و به آدمی با دسترسیِ SSH. حالا یکبار لوکال ساخته میشود و فایلش بالا میرود. همسانسازی با عنوان است نه با id: idها بینِ دو دیتابیس یکی نیستند و تکیه بر آنها یعنی وارد کردن روی سرور محتوای بیربط را بازنویسی میکند. وارد کردنِ برنامهی درسی عمداً چیزی پاک نمیکند و idempotent است — یک فایلِ ناقص نباید محتوای سرور را از بین ببرد، و اجرای دوباره نباید چیزی را دو بار بسازد. (وارد کردنِ سوالها اما replace دارد، چون جایگزینیِ سوالهای یک درس کارِ رایجی است.) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
274 lines
16 KiB
Markdown
274 lines
16 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/` → «وارد کردنِ برنامهی درسی از فایل» |
|
||
| گرفتنِ خروجیِ برنامهی درسی | پایهها را انتخاب کنید → اکشنِ «دریافتِ فایلِ برنامهی درسی» |
|
||
|
||
خروجیِ هر کدام دقیقاً همان قالبیست که ورودیاش میپذیرد، پس چرخهی
|
||
«بگیر → ویرایش کن → برگردان» بسته است و برای گرفتنِ نمونه لازم نیست کسی
|
||
مستندات بخواند.
|
||
|
||
دو تضمین:
|
||
- **سوالها:** فایلِ نامعتبر هیچ چیزی نمینویسد — یا همه وارد میشوند یا هیچکدام.
|
||
- **برنامهی درسی:** چیزی پاک نمیشود. هر چه در فایل هست ساخته/بهروز میشود و
|
||
هر چه نیست دستنخورده میماند، پس یک فایلِ ناقص محتوای سرور را از بین نمیبرد.
|
||
|
||
### دادهی تستی روی سرور
|
||
|
||
`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`).
|