# 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:///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 دیده می‌شوند. محتوای واقعی از مسیرِ جدولِ بالا می‌رود، نه از این دستور. ### ورودِ توسعه‌دهنده‌ها به سرور (بدونِ کاوه‌نگار) تا وقتی `KAVENEGAR_API_KEY` ست نشده، `POST /api/auth/otp/request` خطای صریحِ ۵۰۳ می‌دهد و با `DEBUG=0` هیچ‌کس نمی‌تواند وارد API شود. برای اینکه ورودِ تیمِ فرانت به یک نفرِ خاص وابسته نباشد، `TEST_LOGIN_PHONES` را در `.env` سرور پر کنید: ```bash TEST_LOGIN_PHONES=09120000001:834512,09120000002:110947 ``` هر جفت یک حسابِ تست است: برای آن شماره پیامکی ارسال نمی‌شود و همان کدِ ثابت همیشه معتبر است. هر شماره‌ی دیگری مسیرِ عادیِ OTP را می‌رود، بی‌هیچ تغییری. ```bash curl -X POST https://api.math.tadrisino2.ir/api/auth/otp/request \ -H 'Content-Type: application/json' -d '{"phone":"09120000001"}' curl -X POST https://api.math.tadrisino2.ir/api/auth/login \ -H 'Content-Type: application/json' \ -d '{"phone":"09120000001","otp":"834512"}' ``` چند نکته که عمدی‌اند: - **این با `DEV_MASTER_OTP` یکی نیست.** آن کدِ `0000` هر شماره‌ای را قبول می‌کند و فقط با `DEBUG=1` روشن می‌شود؛ این فقط همین چند شماره را، هرکدام با کدِ خودش. - **کد در پاسخِ API برنمی‌گردد.** کسی که شماره را بداند هم نمی‌تواند کد را از سرور بیرون بکشد، و سقفِ تلاشِ ورودِ هر شماره روی این‌ها هم اعمال می‌شود. - **`/api/health` تا وقتی این متغیر پر باشد هشدار می‌دهد** (تعداد، نه شماره و نه کد) تا روی سروری که کاربرِ واقعی دارد فراموش نشود. - **مسیرِ مهمان دست‌نخورده است.** فرانت که امروز با `POST /api/auth/session` و `deviceId` کار می‌کند، هیچ تغییری نمی‌بیند. - خالی گذاشتنِ این متغیر یعنی رفتارِ قبلی، مو‌به‌مو. ### متغیرهای محیطی موردنیاز | متغیر | لازم در پروداکشن | توضیح | |---|---|---| | `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 از طریق کاوه‌نگار | | `TEST_LOGIN_PHONES` | نه (فقط تا وقتی تیم دارد توسعه می‌دهد) | شماره‌های تستِ ورود، بخشِ بالا | | `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:///api/schema/ # باید 200 برگردونه (OpenAPI schema) curl -i https:///api/docs/ # باید 200 برگردونه (Swagger UI) curl -i -X POST https:///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`).