With DEBUG=0 and Kavenegar unconfigured, POST /api/auth/otp/request returns a hard 503, so the only way to sign in was for someone with admin access to hand- create an OTPCode row. That made every frontend developer's login depend on one person being available. TEST_LOGIN_PHONES is the pattern real apps use for exactly this (Firebase Auth's fictional numbers, Apple's App Store demo account): a short allowlist of phone:code pairs that skip SMS delivery entirely. Unlike DEV_MASTER_OTP it is not "any phone with one code" and it is not tied to DEBUG -- each number carries its own code, and everything else takes the normal OTP path untouched. The code never appears in an API response, so knowing the number is not enough to log in, and the existing per-phone login attempt limit still applies, so the code cannot be brute-forced. Malformed configuration raises ImproperlyConfigured at startup rather than being skipped silently, and /api/health reports how many test numbers are open (count only) so they cannot be forgotten on a server that has real users. The code is capped at six digits because LoginRequestSerializer caps otp at max_length=6; a longer code would have been rejected before reaching the check and looked like the feature was broken. The guest path the frontend uses today is untouched, and leaving the variable empty reproduces the previous behaviour exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
تنظیم محیط
python3 -m venv venv
source venv/bin/activate # ویندوز: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
.env.example (تمام متغیرهایی که واقعاً توسط اپ خونده میشن):
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 استفاده کنید):
export DJANGO_DEBUG=1 # ویندوز (PowerShell): $env:DJANGO_DEBUG=1
DEBUGدیفالتشFalseاست. برای فعالکردنِ کمککنندههای dev-only در Swagger (DEV_MASTER_OTPو فیلدِdevOtpتوی پاسخها)، محلیDJANGO_DEBUG=1رو ست کنید.
سریعترین راه اجرای پروژه
python manage.py migrate
python manage.py runserver
اپ روی http://127.0.0.1:8000 بالا میاد.
مایگریشنها
python manage.py migrate
دیتای تستی (Seed)
یک دیتاست کوچک و idempotent میسازه که همهی اپها رو پوشش میده (کاربر، محتوا، پیشرفت،
اشتراک، پرداخت، تخفیف، دستیار هوش مصنوعی، اعلان، افیلیت، پشتیبانی، خانواده). اجرای مکرر
امن است — با get_or_create هیچ رکورد تکراریای نمیسازه. پیشفرض فقط وقتی DEBUG=True
اجرا میشه، مگر با --force.
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:
POST /api/auth/sessionبا{"type":"guest","deviceId":"d_test1"}→ یکtokenبگیرید.- روی دکمهی Authorize بزنید و فقط خودِ
tokenرو (بدون کلمهی Bearer) وارد کنید. - برای تست لاگین با شماره:
POST /api/auth/otp/requestبزنید، یا در محیط توسعه کد 0000 رو با هر شمارهای درPOST /api/auth/loginامتحان کنید (dev-only، نیاز بهDEBUG=True).
دستورهای پرکاربرد
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).
۲. راهنمای دیپلوی پروداکشن
بالا آوردنِ نسخهی جدید — یک دستور
./deploy.sh
همین. اسکریپت به ترتیب کدِ تازه را میگیرد، وابستگیها را نصب میکند، مهاجرتها را اعمال میکند، فایلهای استاتیک را جمع میکند، سرویس را ریاستارت میکند، و در پایان خودش بررسی میکند که واقعاً بالا آمده یا نه. اگر بالا نیامد، علتهای رایج را چاپ میکند.
چرا این مهم است: قبلاً اینها چند کارِ دستی به ترتیبِ درست بودند و فراموش شدنِ
هر کدام اپ را میخواباند، با خطایی که گویا نبود. مثلاً اضافه شدنِ یک پکیجِ جدید
به requirements.txt بدونِ pip install باعثِ ModuleNotFoundError موقعِ بالا
آمدن میشود و کاربر فقط 502 Bad Gateway میبیند. اسکریپت همیشه pip install
را اجرا میکند تا این حالت اصلاً پیش نیاید.
بارِ اول، نامِ سرویس را بدهید تا دفعههای بعد خودکار ریاستارت شود:
TADRISINO_SERVICE=<نامِ سرویس> ./deploy.sh
متغیرهای اختیاری: SKIP_PULL=1 (بدونِ git pull)، TADRISINO_HEALTH_URL،
PIP_INDEX_URL (اگر pypi.org تایماوت میدهد).
بررسیِ سلامت از بیرون
curl https://<host>/api/health
بدونِ احرازِ هویت و بدونِ SSH میگوید دیتابیس سالم است یا نه، مهاجرتِ
اعمالنشده مانده یا نه، کش کار میکند یا نه، و چند پایه/درس/سوال روی آن سرور
هست. 200 یعنی سالم، 503 یعنی چیزی واقعاً خراب است. هیچ سکرتی برنمیگرداند.
این همان چیزیست که جلوی «کد رفته ولی مهاجرت اجرا نشده» را میگیرد — حالتی که اپ ظاهراً بالا میآید و بعد زیرِ بارِ واقعی میشکند.
مدیریتِ محتوا بدونِ ترمینال
هیچ محتوایی لازم نیست با دستور روی سرور گذاشته شود.
| کار | کجا |
|---|---|
| افزودن/ویرایشِ یک سوال | /admin/content/question/ → «افزودن» (انتخابِ درس با جستجو) |
| وارد کردنِ فلهای سوال | همان صفحه → «وارد کردنِ سوالها از فایل» |
| گرفتنِ خروجیِ سوالها | چند سوال را انتخاب کنید → اکشنِ «دریافتِ فایل» |
| وارد کردنِ برنامهی درسی (پایه/فصل/درس) | /admin/content/grade/ → «وارد کردنِ برنامهی درسی از فایل» |
| گرفتنِ خروجیِ برنامهی درسی | پایهها را انتخاب کنید → اکشنِ «دریافتِ فایلِ برنامهی درسی» |
خروجیِ هر کدام دقیقاً همان قالبیست که ورودیاش میپذیرد، پس چرخهی «بگیر → ویرایش کن → برگردان» بسته است و برای گرفتنِ نمونه لازم نیست کسی مستندات بخواند.
دو تضمین:
- سوالها: فایلِ نامعتبر هیچ چیزی نمینویسد — یا همه وارد میشوند یا هیچکدام.
- برنامهی درسی: چیزی پاک نمیشود. هر چه در فایل هست ساخته/بهروز میشود و هر چه نیست دستنخورده میماند، پس یک فایلِ ناقص محتوای سرور را از بین نمیبرد.
ساختِ انبوهِ سوال از کتاب
وقتی کتابی تازه روی Qdrant آمده و باید برایش سوال ساخته شود، مسیرِ رایگان این است:
# اسکلتِ برنامهی درسی از فهرستِ خودِ کتاب (بدونِ هزینه)
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 سرور پر
کنید:
TEST_LOGIN_PHONES=09120000001:834512,09120000002:110947
هر جفت یک حسابِ تست است: برای آن شماره پیامکی ارسال نمیشود و همان کدِ ثابت همیشه معتبر است. هر شمارهی دیگری مسیرِ عادیِ OTP را میرود، بیهیچ تغییری.
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 اجرا میکنه، نه یک سرورِ پروداکشن).
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 خودش این رو مدیریت میکنه:
- اول
lokiبالا میاد. alloyوgrafanaبعد ازlokiبالا میان (depends_on).djangoهیچ وابستگیای به استکِ لاگ نداره — خودشmigrateبعدrunserverرو اجرا میکنه و منتظرِ Loki/Alloy/Grafana نمیمونه.
دستورهای manage.py داخلِ Docker
اگه فقط از طریقِ Docker اجرا میکنید (بدونِ venv محلی)، python manage.py ... رو مستقیم
روی سیستم خودتون اجرا نکنید — باید داخلِ کانتینرِ django اجرا بشه:
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 اجرا میشه.)
مایگریشنها
python manage.py migrate --noinput
در Docker، این بهصورت خودکار بهعنوان بخشی از دستورِ سرویسِ django، قبل از runserver اجرا میشه.
راهنمای seed برای staging/تست
- هرگز
seed_test_dataرو روی یک دیتابیسِ پروداکشنِ واقعی اجرا نکنید. - روی staging (
DEBUG=False)، صریحاً با--forceاجرا کنید:python manage.py seed_test_data --force - برای پاککردن و ساختِ دوبارهی دیتای seed بین چرخههای تست، از
--resetاستفاده کنید. همهی رکوردهای seedشده در فیلدهای متنیِ قابلمشاهده با تگِ[SEED]مشخص شدن، برای شناساییِ راحتتر و پاکسازی.
چطور API دیپلویشده رو verify کنیم
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).