docs: handbook, diagrams and guides brought up to 2026-10-02
- technical-handbook: question packs instead of subscriptions, real hearts with the new wait screen (screenshot), math-only tutor and what is charged (new 9-7), admin cancel/refund, Bazaar planId, account deletion, 59 endpoints, 779 tests, 14 apps, status and open decisions. - Diagrams redrawn: use-case (buy a pack, refunds), data model (QuestionCredit, HeartsPass, Profile.birth_date), pack payment sequence. - The PDF is now built from the Markdown by docs/build_handbook.py (pandoc + Chrome, font in docs/assets); the hand-made HTML it used to come from was not in the repo and had been lost. - architecture, operations, security-before-production (item 13 closed), ai-cost-and-guardrails and README follow the same changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -12,8 +12,8 @@
|
|||||||
| **وضعیت** | روی پروداکشن، در حالِ سرویسدهی |
|
| **وضعیت** | روی پروداکشن، در حالِ سرویسدهی |
|
||||||
| **آدرسِ زنده** | `https://mathapi.tadrisino2.ir` · [سلامت](https://mathapi.tadrisino2.ir/api/health) |
|
| **آدرسِ زنده** | `https://mathapi.tadrisino2.ir` · [سلامت](https://mathapi.tadrisino2.ir/api/health) |
|
||||||
| **استک** | Python 3.11+ · Django 5.2 · DRF · SQLite · Qdrant (RAG) |
|
| **استک** | Python 3.11+ · Django 5.2 · DRF · SQLite · Qdrant (RAG) |
|
||||||
| **API** | ۶۱ اندپوینت · مستنداتِ زنده در `/api/docs/` |
|
| **API** | ۵۹ اندپوینت · مستنداتِ زنده در `/api/docs/` |
|
||||||
| **تست** | ۶۸۵ تست، همه سبز (`python manage.py test`) |
|
| **تست** | ۷۷۹ تست، همه سبز (`python manage.py test`) |
|
||||||
| **محتوا** | ۸ پایه · ۵۸ فصل · ۲۶۳ نود · ۲٬۶۱۷ درس · ۲٬۵۴۵ سوال |
|
| **محتوا** | ۸ پایه · ۵۸ فصل · ۲۶۳ نود · ۲٬۶۱۷ درس · ۲٬۵۴۵ سوال |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ Message.objects.filter(role='assistant', created_at__gte=since).values('conversa
|
|||||||
`transaction_mode: IMMEDIATE`ِ SQLite همین تست ۵۰۰ میگیرد. روی Postgres سنجیده نشده.
|
`transaction_mode: IMMEDIATE`ِ SQLite همین تست ۵۰۰ میگیرد. روی Postgres سنجیده نشده.
|
||||||
- **قلبِ نامحدودِ بسته** در `HeartsPass` است، جدا از `Subscription`، تا خریدِ بسته اشتراکِ کسی را عوض
|
- **قلبِ نامحدودِ بسته** در `HeartsPass` است، جدا از `Subscription`، تا خریدِ بسته اشتراکِ کسی را عوض
|
||||||
نکند. از پایانِ کارتِ قبلی ادامه پیدا میکند. `HEARTS_ENFORCED` حالا پیشفرض روشن است، پس قلبِ صفر
|
نکند. از پایانِ کارتِ قبلی ادامه پیدا میکند. `HEARTS_ENFORCED` حالا پیشفرض روشن است، پس قلبِ صفر
|
||||||
واقعاً جلوی جواب را میگیرد.
|
واقعاً جلوی جواب را میگیرد، و پایانِ درس یا جزیرهی بیجواب را هم (`content/tests_hearts_gate.py`).
|
||||||
|
- **لغو و بازپرداخت** از پنلِ ادمین (`/admin/payment/order/`): بازپرداخت فقط اعتبارِ خرجنشدهی همان
|
||||||
|
بسته و روزهای قلبش را پس میگیرد؛ پول از پنلِ درگاه برگردانده میشود. روند در `docs/operations.md`.
|
||||||
- اپ موجودی را از `GET /api/ai/quota` میخواند و بعد از هر جواب از `remainingQuota` و `credits`ِ خودِ
|
- اپ موجودی را از `GET /api/ai/quota` میخواند و بعد از هر جواب از `remainingQuota` و `credits`ِ خودِ
|
||||||
جواب. صفحهی خرید `GET /api/subscription/status` (`questionCredits`، `unlimitedHeartsUntil`) را میخواند.
|
جواب. صفحهی خرید `GET /api/subscription/status` (`questionCredits`، `unlimitedHeartsUntil`) را میخواند.
|
||||||
- **کافهبازار:** هر بسته باید در پنلِ بازار «محصولِ مصرفی» تعریف شود و اپِ اندروید بعد از تأیید
|
- **کافهبازار:** هر بسته باید در پنلِ بازار «محصولِ مصرفی» تعریف شود و اپِ اندروید بعد از تأیید
|
||||||
@@ -77,7 +79,10 @@ Message.objects.filter(role='assistant', created_at__gte=since).values('conversa
|
|||||||
1. **صافیِ سرور، پیش از مدل و بیهزینه.** فقط ترفندهای آشکار را میگیرد: «دستورهای قبلی را نادیده
|
1. **صافیِ سرور، پیش از مدل و بیهزینه.** فقط ترفندهای آشکار را میگیرد: «دستورهای قبلی را نادیده
|
||||||
بگیر»، «پرامپتت چیه»، `developer mode`. این صافی عمداً تنگ است: «قانونِ فیثاغورس را فراموش کردم»
|
بگیر»، «پرامپتت چیه»، `developer mode`. این صافی عمداً تنگ است: «قانونِ فیثاغورس را فراموش کردم»
|
||||||
ریاضی است و رد نمیشود. جملهای دربارهی آسیب به خود رد نمیشود و جوابی مهربان با شمارهی
|
ریاضی است و رد نمیشود. جملهای دربارهی آسیب به خود رد نمیشود و جوابی مهربان با شمارهی
|
||||||
اورژانسِ اجتماعی (۱۲۳) و صدای مشاور (۱۴۸۰) میگیرد.
|
اورژانسِ اجتماعی (۱۲۳) و صدای مشاور (۱۴۸۰) میگیرد. تا ۲۰۲۶-۱۰-۰۲ «می خوام بمیرم» (با فاصله)،
|
||||||
|
«دیگه نمیخوام زنده باشم» و «خود کشی» از صافی رد میشدند و مدل فقط جوابِ «فقط ریاضی» میداد
|
||||||
|
(یک فراخوانیِ زنده، ۰٫۰۱۸ سنت). حالا روی متنِ بیفاصله سنجیده میشود. سوالی مثلِ «امروز پام درد
|
||||||
|
میکنه» به مدل میرسد و متنِ ثابتِ «فقط ریاضی» را میگیرد (آزموده شد).
|
||||||
2. **قاعده در پرامپتِ سیستم (`SCOPE_RULE`):** هر چیزِ غیرِ ریاضی، و هر دستوری از درونِ پیامِ
|
2. **قاعده در پرامپتِ سیستم (`SCOPE_RULE`):** هر چیزِ غیرِ ریاضی، و هر دستوری از درونِ پیامِ
|
||||||
دانشآموز، با علامتِ `[خارج-از-ریاضی]` جواب داده میشود.
|
دانشآموز، با علامتِ `[خارج-از-ریاضی]` جواب داده میشود.
|
||||||
3. **جایگزینیِ علامت با متنِ ثابت.** در استریم، ۴۰ حرفِ اول نگه داشته میشود تا علامت هرگز به
|
3. **جایگزینیِ علامت با متنِ ثابت.** در استریم، ۴۰ حرفِ اول نگه داشته میشود تا علامت هرگز به
|
||||||
|
|||||||
@@ -260,18 +260,24 @@ temperature و با `OPENAI_CHAT_REASONING_EFFORT` صدا زده میشوند
|
|||||||
|
|
||||||
## قلبها، مثلِ دولینگو
|
## قلبها، مثلِ دولینگو
|
||||||
|
|
||||||
هر پاسخِ غلط یک قلب کم میکند (مشترکِ فعال قلب از دست نمیدهد). **هر ۵ ساعت یک
|
هر پاسخِ غلط یک قلب کم میکند (اشتراکِ فعال یا روزهای قلبِ بستهی سوال = قلبِ نامحدود). **هر ۵ ساعت یک
|
||||||
قلب برمیگردد** (`HEART_REFILL_INTERVAL`) و **۲۴ ساعت بعد از اولین قلبی که از
|
قلب برمیگردد** (`HEART_REFILL_INTERVAL`) و **۲۴ ساعت بعد از اولین قلبی که از
|
||||||
حالتِ پر کم شد، همه یکجا پر میشوند** (`HEART_FULL_REFILL_AFTER`، فیلدِ
|
حالتِ پر کم شد، همه یکجا پر میشوند** (`HEART_FULL_REFILL_AFTER`، فیلدِ
|
||||||
`full_refill_at`). `GET /api/user/hearts` هر دو زمان را میدهد: `nextRefillAt`
|
`full_refill_at`). `GET /api/user/hearts` هر دو زمان را میدهد: `nextRefillAt`
|
||||||
و `fullRefillAt`. تا ۲۰۲۶-۰۹-۲۴ هر قلب ۲۴ ساعت بود؛ تایمرِ قدیمیِ بلندتر از ۵
|
و `fullRefillAt`. تا ۲۰۲۶-۰۹-۲۴ هر قلب ۲۴ ساعت بود؛ تایمرِ قدیمیِ بلندتر از ۵
|
||||||
ساعت در اولین خواندن کوتاه میشود.
|
ساعت در اولین خواندن کوتاه میشود.
|
||||||
|
|
||||||
**با صفر قلب، بکاند فقط وقتی جواب را رد میکند که `HEARTS_ENFORCED=1` باشد**
|
**با صفر قلب، بکاند جواب را رد میکند** (۴۰۳ با `code: "no_hearts"`، `nextRefillAt` و
|
||||||
(۴۰۳ با `code: "no_hearts"` و `nextRefillAt`). پیشفرض خاموش است چون فرانتِ فعلی
|
`fullRefillAt`). `HEARTS_ENFORCED` از ۲۰۲۶-۱۰-۰۲ پیشفرض روشن است و فرانت برای این پاسخ صفحهی
|
||||||
برای این پاسخ صفحه ندارد و دانشآموزِ بیقلب را به صفحهی خطای عمومی میبرد. تا
|
«قلبهات تموم شد» را با شمارشِ معکوس دارد (`src/components/NoHeartsScreen.tsx`). از همان روز:
|
||||||
روشن نشده، «قلبِ نامحدودِ» اشتراک فقط در ظاهرِ اپ اعمال میشود و با صدا زدنِ
|
- چکِ قلب داخلِ همان تراکنشی است که قلب را کم میکند. پیشتر بیرونش بود و دو جوابِ غلطِ همزمان با
|
||||||
مستقیمِ API دور زده میشود.
|
یک قلبِ باقیمانده هر دو پذیرفته میشدند (با دو thread بازتولید شد).
|
||||||
|
- پایانِ درس یا جزیره بیقلب و بیجواب دادنِ سوالهایش ۴۰۳ میگیرد؛ پیشتر `quiz/complete` اجازه
|
||||||
|
میداد با قلبِ صفر جزیرهبهجزیره جلو رفت. کسی که آخرین قلبش را روی آخرین سوال از دست داده،
|
||||||
|
همه را جواب داده و جزیره را تمام میکند.
|
||||||
|
- قلبی که در انتظار برگشته پیش از جواب ذخیره میشود؛ پیشتر پاسخِ جوابِ درست «۰ قلب» میگفت و
|
||||||
|
اپ دانشآموز را دوباره به صفحهی انتظار میبرد.
|
||||||
|
تستها: `content/tests_hearts_gate.py`.
|
||||||
|
|
||||||
**امتیاز فقط برای جوابِ ثبتشده است.** `lessons/{id}/complete` و
|
**امتیاز فقط برای جوابِ ثبتشده است.** `lessons/{id}/complete` و
|
||||||
`nodes/{id}/quiz/complete` عددِ `correctCount`ِ کلاینت را به تعدادِ سوالهای
|
`nodes/{id}/quiz/complete` عددِ `correctCount`ِ کلاینت را به تعدادِ سوالهای
|
||||||
|
|||||||
BIN
docs/assets/Vazirmatn.woff2
Normal file
95
docs/build_handbook.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""
|
||||||
|
مستندِ فنی را از Markdown به PDF میسازد: docs/technical-handbook.md → docs/technical-handbook.pdf
|
||||||
|
|
||||||
|
python docs/build_handbook.py
|
||||||
|
|
||||||
|
چرا (۱۰ مهر ۱۴۰۵): PDF پیشتر از یک HTMLِ دستسازِ جدا ساخته میشد که در مخزن نبود. هر بهروزرسانی
|
||||||
|
یعنی ویرایشِ دو نسخه، و بعد از ۹ مهر آن HTML گم شد. حالا Markdown تنها منبع است و PDF از روی آن
|
||||||
|
ساخته میشود؛ نمودارها PNGهای `docs/images/technical-handbook/` هستند.
|
||||||
|
|
||||||
|
نیاز دارد: `pandoc` (brew install pandoc) و Google Chrome. فونت (وزیرمتن، مجوزِ OFL) کنارِ همین فایل در
|
||||||
|
`docs/assets/` است، پس PDF روی هر ماشینی یکسان درمیآید و به اینترنت نیاز ندارد.
|
||||||
|
"""
|
||||||
|
import base64
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DOCS = Path(__file__).resolve().parent
|
||||||
|
SRC = DOCS / 'technical-handbook.md'
|
||||||
|
OUT = DOCS / 'technical-handbook.pdf'
|
||||||
|
CHROME_CANDIDATES = [
|
||||||
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
||||||
|
'google-chrome', 'chromium', 'chromium-browser',
|
||||||
|
]
|
||||||
|
|
||||||
|
CSS = '''
|
||||||
|
@font-face{font-family:'Vazirmatn';font-weight:100 900;src:url(data:font/woff2;base64,%(font)s) format('woff2')}
|
||||||
|
@page{size:A4;margin:16mm 15mm 16mm 15mm}
|
||||||
|
:root{--ink:#1d2330;--muted:#5b6472;--line:#e4e0d6;--accent:#2f55c8;--accent-soft:#eaf0ff;--code-bg:#f6f4ee}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{background:#fff}
|
||||||
|
body{margin:0;font-family:Vazirmatn,Tahoma,sans-serif;color:var(--ink);font-size:10.6pt;line-height:1.85;
|
||||||
|
-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
||||||
|
h1{font-size:25pt;font-weight:900;margin:0 0 6px;color:var(--ink)}
|
||||||
|
h1+table{margin-top:14px}
|
||||||
|
h2{font-size:17pt;font-weight:800;margin:0 0 8px;padding-top:6px;border-top:3px solid var(--accent);break-before:page;break-after:avoid}
|
||||||
|
h3{font-size:12.5pt;font-weight:700;margin:20px 0 6px;color:#26305a;break-after:avoid}
|
||||||
|
h4{font-size:11pt;font-weight:700;margin:12px 0 4px;break-after:avoid}
|
||||||
|
p{margin:6px 0}
|
||||||
|
a{color:var(--accent);text-decoration:none}
|
||||||
|
code{font-family:Menlo,Consolas,monospace;font-size:.84em;direction:ltr;unicode-bidi:isolate;background:var(--code-bg);
|
||||||
|
padding:1px 4px;border-radius:4px;border:1px solid #ece8dd}
|
||||||
|
pre{direction:ltr;text-align:left;background:#1f2430;color:#e8e6df;border-radius:8px;padding:10px 12px;
|
||||||
|
font-size:8.6pt;line-height:1.55;white-space:pre-wrap;word-break:break-word;break-inside:avoid}
|
||||||
|
pre code{background:none;border:0;padding:0;color:inherit;font-size:1em}
|
||||||
|
table{width:100%%;border-collapse:collapse;margin:8px 0 12px;font-size:9.4pt;break-inside:auto}
|
||||||
|
th,td{border:1px solid var(--line);padding:5px 8px;text-align:right;vertical-align:top}
|
||||||
|
th{background:#f4f1e8;font-weight:700}
|
||||||
|
tr{break-inside:avoid}
|
||||||
|
blockquote{margin:10px 0;padding:8px 14px;border:1px solid #d9e2fb;border-right:4px solid var(--accent);
|
||||||
|
background:var(--accent-soft);border-radius:8px;break-inside:avoid}
|
||||||
|
blockquote h4{margin-top:2px;color:var(--accent)}
|
||||||
|
img{max-width:100%%;display:block;margin:10px auto 4px;border-radius:6px}
|
||||||
|
img[src*="screenshot-"]{max-width:62mm;border:1px solid var(--line);box-shadow:0 2px 8px rgba(30,40,80,.08)}
|
||||||
|
p:has(> img){break-inside:avoid;margin:0}
|
||||||
|
p > em:only-child{display:block;font-size:9pt;color:var(--muted);text-align:center;margin:2px 0 12px}
|
||||||
|
ul,ol{padding-right:20px;margin:6px 0}
|
||||||
|
li{margin:2px 0}
|
||||||
|
hr{border:0;border-top:1px solid var(--line);margin:14px 0}
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def chrome() -> str:
|
||||||
|
for c in CHROME_CANDIDATES:
|
||||||
|
if Path(c).exists() or shutil.which(c):
|
||||||
|
return c
|
||||||
|
sys.exit('Chrome پیدا نشد؛ مسیرش را در CHROME_CANDIDATES بگذارید.')
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not shutil.which('pandoc'):
|
||||||
|
sys.exit('pandoc نصب نیست: brew install pandoc')
|
||||||
|
font = base64.b64encode((DOCS / 'assets' / 'Vazirmatn.woff2').read_bytes()).decode()
|
||||||
|
body = subprocess.run(['pandoc', str(SRC), '-f', 'gfm', '-t', 'html5'],
|
||||||
|
capture_output=True, text=True, check=True).stdout
|
||||||
|
html = (f'<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8">'
|
||||||
|
f'<title>مستند فنی تدریسینو</title><style>{CSS % {"font": font}}</style></head>'
|
||||||
|
f'<body>{body}</body></html>')
|
||||||
|
# فایلِ موقت کنارِ docs تا مسیرِ نسبیِ عکسها (images/…) درست باز شود.
|
||||||
|
with tempfile.NamedTemporaryFile('w', suffix='.html', dir=DOCS, delete=False, encoding='utf-8') as f:
|
||||||
|
f.write(html)
|
||||||
|
tmp = Path(f.name)
|
||||||
|
try:
|
||||||
|
subprocess.run([chrome(), '--headless=new', '--disable-gpu', '--no-pdf-header-footer',
|
||||||
|
'--virtual-time-budget=10000', f'--print-to-pdf={OUT}', tmp.as_uri()],
|
||||||
|
check=True, capture_output=True)
|
||||||
|
finally:
|
||||||
|
tmp.unlink(missing_ok=True)
|
||||||
|
print(f'{OUT} ({OUT.stat().st_size // 1024} KB)')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
Before Width: | Height: | Size: 346 KiB After Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 226 KiB After Width: | Height: | Size: 224 KiB |
BIN
docs/images/technical-handbook/screenshot-06-no-hearts.jpg
Normal file
|
After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 212 KiB After Width: | Height: | Size: 214 KiB |
@@ -111,7 +111,7 @@ curl https://mathapi.tadrisino2.ir/api/health
|
|||||||
| `BAZAAR_PISHKHAN_API_SECRET` (روشِ جدید) یا `BAZAAR_CLIENT_ID` · `BAZAAR_CLIENT_SECRET` (روشِ قدیمی)، بهعلاوهی `BAZAAR_PACKAGE_NAME` | `POST /api/payment/bazaar/verify` هر خرید را نامعتبر میداند (fail-closed)؛ `/api/health` → `bazaar: false` |
|
| `BAZAAR_PISHKHAN_API_SECRET` (روشِ جدید) یا `BAZAAR_CLIENT_ID` · `BAZAAR_CLIENT_SECRET` (روشِ قدیمی)، بهعلاوهی `BAZAAR_PACKAGE_NAME` | `POST /api/payment/bazaar/verify` هر خرید را نامعتبر میداند (fail-closed)؛ `/api/health` → `bazaar: false` |
|
||||||
| `OPENAI_API_KEY` · `QDRANT_API_KEY` | `POST /api/ai/ask` همان پاسخِ stub را میدهد |
|
| `OPENAI_API_KEY` · `QDRANT_API_KEY` | `POST /api/ai/ask` همان پاسخِ stub را میدهد |
|
||||||
| `TEST_LOGIN_PHONES` | هیچ حسابِ تستی باز نیست (بخشِ بعد) |
|
| `TEST_LOGIN_PHONES` | هیچ حسابِ تستی باز نیست (بخشِ بعد) |
|
||||||
| `HEARTS_ENFORCED` | خاموش: با صفر قلب هم میشود جواب داد. `1` وقتی فرانت برای ۴۰۳ِ `no_hearts` صفحهی «قلبها تمام شد» دارد |
|
| `HEARTS_ENFORCED` | روشن (از ۲۰۲۶-۱۰-۰۲): با صفر قلب نه جواب پذیرفته میشود نه پایانِ درس/جزیرهی بیجواب. `0` خاموشش میکند |
|
||||||
| `INTERNAL_SERVICE_KEY` | endpointهای داخلیِ ingestِ RAG و ساختِ سفارشِ سرور-به-سرور بستهاند |
|
| `INTERNAL_SERVICE_KEY` | endpointهای داخلیِ ingestِ RAG و ساختِ سفارشِ سرور-به-سرور بستهاند |
|
||||||
|
|
||||||
**اتصال به بازار — روشِ جدید (ترجیحی):** در پیشخانِ بازار روی برنامه ← «API پیشخان
|
**اتصال به بازار — روشِ جدید (ترجیحی):** در پیشخانِ بازار روی برنامه ← «API پیشخان
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
| ۱۰ | ورودِ مهمان فقط با `deviceId` است | کم (طراحی) | کد |
|
| ۱۰ | ورودِ مهمان فقط با `deviceId` است | کم (طراحی) | کد |
|
||||||
| ۱۱ | توکنِ refresh و کدِ OTP بیهش در دیتابیس | کم | کد |
|
| ۱۱ | توکنِ refresh و کدِ OTP بیهش در دیتابیس | کم | کد |
|
||||||
| ۱۲ | هزینهی دستیارِ AI با ساختِ حسابِ مهمان قابلِ بالا بردن است | کم (مالی) | کد |
|
| ۱۲ | هزینهی دستیارِ AI با ساختِ حسابِ مهمان قابلِ بالا بردن است | کم (مالی) | کد |
|
||||||
| ۱۳ | محدودیتِ قلب در سرور خاموش است (`HEARTS_ENFORCED=0`) | کم (کسبوکار) | `.env` + فرانت |
|
| ۱۳ | ~~محدودیتِ قلب در سرور خاموش است (`HEARTS_ENFORCED=0`)~~ **بسته شد ۲۰۲۶-۱۰-۰۲** | کم (کسبوکار) | `.env` + فرانت |
|
||||||
| ۱۴ | SQLite روی دیسکِ مشترکِ پُر، بدونِ بکاپِ خودکارِ بیرون از ماشین | کم (داده) | زیرساخت |
|
| ۱۴ | SQLite روی دیسکِ مشترکِ پُر، بدونِ بکاپِ خودکارِ بیرون از ماشین | کم (داده) | زیرساخت |
|
||||||
| ۱۵ | دسترسیِ سرور: حسابِ مشترک، رمز، sudoِ کامل | بالا | ادمینِ سرور |
|
| ۱۵ | دسترسیِ سرور: حسابِ مشترک، رمز، sudoِ کامل | بالا | ادمینِ سرور |
|
||||||
| ۱۶ | فرانت توکن را در `localStorage` نگه میدارد | کم | فرانت |
|
| ۱۶ | فرانت توکن را در `localStorage` نگه میدارد | کم | فرانت |
|
||||||
@@ -166,16 +166,17 @@
|
|||||||
|
|
||||||
## ۱۲. هزینهی دستیارِ AI
|
## ۱۲. هزینهی دستیارِ AI
|
||||||
|
|
||||||
- **چیست:** سقفِ روزانه ۱۵ سؤال **برای هر کاربر** (`DAILY_AI_QUOTA`) و ۲۰ در ساعت؛ مهمانها
|
- **چیست:** از ۲۰۲۶-۱۰-۰۲ هر کاربر روزی ۳ سؤالِ رایگان دارد (`AI_DAILY_QUOTA_FREE`؛ پیشتر ۱۵)
|
||||||
بر اساسِ IP محدود میشوند ولی هر IP ساعتی ۲۰ حسابِ مهمانِ تازه میسازد (`guest_session`)،
|
و بعد فقط با اعتبارِ خریدهشده؛ ۲۰ در ساعت. مهمانها بر اساسِ IP محدود میشوند ولی هر IP ساعتی
|
||||||
و هر حسابِ تازه ۱۵ سؤالِ تازه دارد. طولِ سؤال از ۲۰۲۶-۰۹-۲۵ حداکثر ۲۰۰۰ نویسه است.
|
۲۰ حسابِ مهمانِ تازه میسازد (`guest_session`)، و هر حسابِ تازه ۳ سؤالِ رایگانِ تازه دارد. خطر
|
||||||
|
کمتر شد (۳ بهجای ۱۵)، ولی بسته نیست. طولِ سؤال از ۲۰۲۶-۰۹-۲۵ حداکثر ۲۰۰۰ نویسه است.
|
||||||
- **خطر:** با چند IP، اعتبارِ GapGPT در چند ساعت تمام میشود (اعتبار یک بار در ۲۰۲۶-۰۹-۲۵
|
- **خطر:** با چند IP، اعتبارِ GapGPT در چند ساعت تمام میشود (اعتبار یک بار در ۲۰۲۶-۰۹-۲۵
|
||||||
به صفر رسید، البته به دلیلِ دیگر).
|
به صفر رسید، البته به دلیلِ دیگر).
|
||||||
- **رفع:** سقفِ سراسریِ روزانه برای کلِ اپ؛ دستیار فقط برای کاربرِ واردشده با شماره یا
|
- **رفع:** سقفِ سراسریِ روزانه برای کلِ اپ؛ دستیار فقط برای کاربرِ واردشده با شماره یا
|
||||||
با سقفِ بسیار کمتر برای مهمان؛ هشدار وقتی `aiProvider.state` در `/api/health` برابرِ
|
با سقفِ بسیار کمتر برای مهمان؛ هشدار وقتی `aiProvider.state` در `/api/health` برابرِ
|
||||||
`quota_exhausted` شد.
|
`quota_exhausted` شد.
|
||||||
|
|
||||||
## ۱۳. محدودیتِ قلب خاموش است
|
## ۱۳. ~~محدودیتِ قلب خاموش است~~ — بسته شد ۲۰۲۶-۱۰-۰۲
|
||||||
|
|
||||||
- **چیست:** با صفر قلب، سرور همچنان جواب را میپذیرد، مگر `HEARTS_ENFORCED=1`
|
- **چیست:** با صفر قلب، سرور همچنان جواب را میپذیرد، مگر `HEARTS_ENFORCED=1`
|
||||||
(`content/views.py::QuestionAnswerView`). «قلبِ نامحدود» تنها مزیتِ اشتراک است.
|
(`content/views.py::QuestionAnswerView`). «قلبِ نامحدود» تنها مزیتِ اشتراک است.
|
||||||
@@ -183,6 +184,9 @@
|
|||||||
- **چرا باز مانده:** فرانت برای ۴۰۳ِ `no_hearts` هنوز صفحه ندارد؛ روشن کردنش دانشآموز را به
|
- **چرا باز مانده:** فرانت برای ۴۰۳ِ `no_hearts` هنوز صفحه ندارد؛ روشن کردنش دانشآموز را به
|
||||||
صفحهی خطا میبرد. رفتار با صفر قلب هم تصمیمِ محصولیِ باز است.
|
صفحهی خطا میبرد. رفتار با صفر قلب هم تصمیمِ محصولیِ باز است.
|
||||||
- **رفع:** بعد از صفحهی «قلبها تمام شد» در فرانت، `HEARTS_ENFORCED=1` در `.env`.
|
- **رفع:** بعد از صفحهی «قلبها تمام شد» در فرانت، `HEARTS_ENFORCED=1` در `.env`.
|
||||||
|
- **بسته شد ۲۰۲۶-۱۰-۰۲** (بکاند `4700ccb`، فرانت `33f5f8d`): `HEARTS_ENFORCED` پیشفرض روشن؛ چکِ
|
||||||
|
قلب داخلِ تراکنش؛ پایانِ درس/جزیرهی بیجواب با قلبِ صفر هم بسته؛ صفحهی «قلبهات تموم شد» با
|
||||||
|
شمارشِ معکوس. تستها: `content/tests_hearts_gate.py`.
|
||||||
|
|
||||||
## ۱۴. دیتابیس و بکاپ
|
## ۱۴. دیتابیس و بکاپ
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| تاریخ | ۸ مهر ۱۴۰۵ (۳۰ سپتامبر ۲۰۲۶) |
|
| تاریخ | ۱۰ مهر ۱۴۰۵ (۲ اکتبر ۲۰۲۶) |
|
||||||
| نویسنده | علیرضا زارعیان — بکاند و فرانتاند |
|
| نویسنده | علیرضا زارعیان — بکاند و فرانتاند |
|
||||||
| مخاطب | توسعهدهندهی تازهوارد، مدیرِ فنی، و هر کسی که باید دربارهی ادامهی پروژه تصمیم بگیرد |
|
| مخاطب | توسعهدهندهی تازهوارد، مدیرِ فنی، و هر کسی که باید دربارهی ادامهی پروژه تصمیم بگیرد |
|
||||||
| کدِ مبنا | بکاند `957c9d0` (مخزنِ `Tadrisino/MathApp` ) · فرانت `0e56d9c` (مخزنِ `Tadrisino/MathApp_frontend` ) |
|
| کدِ مبنا | بکاند `8a4753b` (مخزنِ `Tadrisino/MathApp` ) · فرانت `33f5f8d` (مخزنِ `Tadrisino/MathApp_frontend` ) |
|
||||||
| نشانیها | اپ: `math.tadrisino2.ir` · API: `mathapi.tadrisino2.ir` · مستنداتِ زندهی API: `/api/docs/` |
|
| نشانیها | اپ: `math.tadrisino2.ir` · API: `mathapi.tadrisino2.ir` · مستنداتِ زندهی API: `/api/docs/` |
|
||||||
|
|
||||||
> این فایل نسخهی Markdownِ «مستند-فنی-تدریسینو» است. نسخهی PDF با همان محتوا و صفحهآراییِ کامل کنارِ همین فایل است: `technical-handbook.pdf`.
|
> این فایل نسخهی Markdownِ «مستند-فنی-تدریسینو» است. نسخهی PDF با همان محتوا و صفحهآراییِ کامل کنارِ همین فایل است: `technical-handbook.pdf`.
|
||||||
@@ -20,9 +20,9 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| سوم، ششم، هفتم، هشتم تا دوازدهم | **۸ پایه** |
|
| سوم، ششم، هفتم، هشتم تا دوازدهم | **۸ پایه** |
|
||||||
| سؤالِ چهارگزینهای از مسئلههای کتاب | **۲٬۵۴۵** |
|
| سؤالِ چهارگزینهای از مسئلههای کتاب | **۲٬۵۴۵** |
|
||||||
| اندپوینتِ API، مستند در Swagger | **۶۱** |
|
| اندپوینتِ API، مستند در Swagger | **۵۹** |
|
||||||
| تستِ خودکار، همه سبز | **۶۸۵** |
|
| تستِ خودکار، همه سبز | **۷۷۹** |
|
||||||
| اپِ جنگو، هرکدام یک مسئولیت | **۱۵** |
|
| اپِ جنگو، هرکدام یک مسئولیت | **۱۴** |
|
||||||
| تکهی کتاب در Qdrant (۱۱ collection) | **۳٬۵۱۷** |
|
| تکهی کتاب در Qdrant (۱۱ collection) | **۳٬۵۱۷** |
|
||||||
| همراهِ گیمیفیکیشن، هرکدام ۶ حالتِ چهره | **۴** |
|
| همراهِ گیمیفیکیشن، هرکدام ۶ حالتِ چهره | **۴** |
|
||||||
| بکاند و فرانت؛ یک قراردادِ API | **۲ مخزن** |
|
| بکاند و فرانت؛ یک قراردادِ API | **۲ مخزن** |
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
|
|
||||||
### درونِ بکاند: «یک برنامه، چند اپِ مستقل» (Modular Monolith)
|
### درونِ بکاند: «یک برنامه، چند اپِ مستقل» (Modular Monolith)
|
||||||
|
|
||||||
بکاند یک برنامهی جنگوست که به ۱۵ «اپ» تقسیم شده. هر اپ یک پوشه است با مدلها، APIها و تستهای خودش. این همان الگویی است که اجازه میدهد چند نفر همزمان روی بکاند کار کنند (بخشِ ۱۴).
|
بکاند یک برنامهی جنگوست که به ۱۴ «اپ» تقسیم شده (اپِ `family` ۱۰ مهر ۱۴۰۵ حذف شد). هر اپ یک پوشه است با مدلها، APIها و تستهای خودش. این همان الگویی است که اجازه میدهد چند نفر همزمان روی بکاند کار کنند (بخشِ ۱۴).
|
||||||
|
|
||||||
| اپ | مسئولیت | مدلها (جدولها) |
|
| اپ | مسئولیت | مدلها (جدولها) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -105,8 +105,8 @@
|
|||||||
| `progress` | امتیاز، سکه، قلب، زنجیره، جدولِ رتبه، همراهها | `UserStats UserHearts StreakRecord ActiveDay LeaderboardEntry Character` |
|
| `progress` | امتیاز، سکه، قلب، زنجیره، جدولِ رتبه، همراهها | `UserStats UserHearts StreakRecord ActiveDay LeaderboardEntry Character` |
|
||||||
| `ai_tutor` | گفتگو با دستیار، سهمیه، بازخوردِ 👍/👎 | `Conversation Message MessageFeedback AskIdempotency` |
|
| `ai_tutor` | گفتگو با دستیار، سهمیه، بازخوردِ 👍/👎 | `Conversation Message MessageFeedback AskIdempotency` |
|
||||||
| `rag` | خواندنِ کتاب، Qdrant، ساختِ پرامپت، شکلها | `LessonChunk LessonChunkImage IngestedPage` |
|
| `rag` | خواندنِ کتاب، Qdrant، ساختِ پرامپت، شکلها | `LessonChunk LessonChunkImage IngestedPage` |
|
||||||
| `subscription` | پلنها و اشتراکِ کاربر | `Plan Subscription` |
|
| `subscription` | پلنها (بستهی سوال، اشتراکِ قدیمی)، دفترِ اعتبارِ سوال، قلبِ نامحدودِ بسته | `Plan Subscription QuestionCredit HeartsPass` |
|
||||||
| `payment` | سفارش، تراکنش، زرینپال، بازار | `Order Transaction BazaarCredential` |
|
| `payment` | سفارش، تراکنش، زرینپال، بازار، لغو و بازپرداخت از ادمین | `Order Transaction BazaarCredential` |
|
||||||
| `discount` | کدِ تخفیف و پیشنهادها | `DiscountCode Redemption Offer UserOffer` |
|
| `discount` | کدِ تخفیف و پیشنهادها | `DiscountCode Redemption Offer UserOffer` |
|
||||||
| `affiliate` | معرفی به دوستان و پورسانت | `AffiliateAccount Conversion` |
|
| `affiliate` | معرفی به دوستان و پورسانت | `AffiliateAccount Conversion` |
|
||||||
| `notifications` | اعلانهای داخلِ اپ | `Notification` |
|
| `notifications` | اعلانهای داخلِ اپ | `Notification` |
|
||||||
@@ -314,18 +314,22 @@ with transaction.atomic():
|
|||||||
|
|
||||||
سه نکتهی طراحی: (۱) تماس با GapGPT **بیرون از تراکنشِ دیتابیس** است، وگرنه ۳۰ ثانیه انتظار دیتابیس را قفل میکرد؛ (۲) اگر GapGPT خطا داد، سهمیهی رزروشده پس داده میشود و کاربر ۵۰۳ میگیرد، نه ۵۰۰؛ (۳) اولین پیامِ هر گفتگو کش میشود — سؤالِ تکراری از کش ۰٫۰۱ ثانیهای برمیگردد در برابرِ ~۱۱ ثانیه.
|
سه نکتهی طراحی: (۱) تماس با GapGPT **بیرون از تراکنشِ دیتابیس** است، وگرنه ۳۰ ثانیه انتظار دیتابیس را قفل میکرد؛ (۲) اگر GapGPT خطا داد، سهمیهی رزروشده پس داده میشود و کاربر ۵۰۳ میگیرد، نه ۵۰۰؛ (۳) اولین پیامِ هر گفتگو کش میشود — سؤالِ تکراری از کش ۰٫۰۱ ثانیهای برمیگردد در برابرِ ~۱۱ ثانیه.
|
||||||
|
|
||||||
### ۶-۴ خریدِ اشتراک با زرینپال
|
### ۶-۴ خریدِ بستهی سوال با زرینپال
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
*کد: `subscription/views.py` · `payment/services.py` · `payment/views.py` ( `PaymentRedirectView`، `ZarinpalCallbackView` ) · `payment/zarinpal.py` · تستها: `payment/tests_audit_fixes.py`*
|
*کد: `subscription/views.py` · `payment/services.py` · `subscription/services.py → grant_pack` · `payment/views.py` ( `PaymentRedirectView`، `ZarinpalCallbackView` ) · `payment/zarinpal.py` · تستها: `payment/tests_audit_fixes.py` · `subscription/tests_packs.py` · `payment/tests_refund.py`*
|
||||||
|
|
||||||
|
از ۱۰ مهر ۱۴۰۵ آنچه فروخته میشود **بستهی سوال** است، نه اشتراکِ ماهانه/سالانه: ۲۰ / ۶۰ / ۱۵۰ سوال از دستیار به ۴۵ / ۱۱۹ / ۲۵۹ هزار تومان، بیانقضا، با ۷ / ۳۰ / ۶۰ روز قلبِ نامحدودِ هدیه. پرداختِ تأییدشده یک ردیفِ مثبت در دفترِ `QuestionCredit` و روزهای `HeartsPass` میسازد و به اشتراکِ کسی دست نمیزند. پلنهای اشتراکیِ قبلی پاک نشدند ( `on_sale=False` ) تا مشترکِ فعلی سقفش را نگه دارد.
|
||||||
|
|
||||||
> #### چرا اینقدر پیچیده؟
|
> #### چرا اینقدر پیچیده؟
|
||||||
>
|
>
|
||||||
> - **ساختِ سفارش به زرینپال وابسته نیست:** اگر زرینپال قطع باشد، سفارش ساخته میشود و فقط رفتن به درگاه عقب میافتد.
|
> - **ساختِ سفارش به زرینپال وابسته نیست:** اگر زرینپال قطع باشد، سفارش ساخته میشود و فقط رفتن به درگاه عقب میافتد.
|
||||||
> - **برگشتِ کاربر هیچ چیزی را ثابت نمیکند:** هر کس میتواند آدرسِ برگشت را با `Status=OK` بسازد. فقط تأییدِ سرور-به-سرور سفارش را «پرداختشده» میکند.
|
> - **برگشتِ کاربر هیچ چیزی را ثابت نمیکند:** هر کس میتواند آدرسِ برگشت را با `Status=OK` بسازد. فقط تأییدِ سرور-به-سرور سفارش را «پرداختشده» میکند.
|
||||||
> - **پرداختِ جاافتاده بازسنجی میشود:** اگر زرینپال موقعِ برگشت جواب نداد، هر بار که اپ وضعیتِ اشتراک را میخواند سفارش دوباره سنجیده میشود (و دستورِ `payment_reconcile` ).
|
> - **پرداختِ جاافتاده بازسنجی میشود:** اگر زرینپال موقعِ برگشت جواب نداد، هر بار که اپ وضعیتِ خرید را میخواند سفارش دوباره سنجیده میشود (و دستورِ `payment_reconcile` ).
|
||||||
> - **پلن روی خودِ سفارش است** ؛ کدِ تخفیفِ ۱۰۰٪ بیدرگاه تمام میکند؛ تمدید از تاریخِ انقضای فعلی حساب میشود.
|
> - **پلن روی خودِ سفارش است** ؛ کدِ تخفیفِ ۱۰۰٪ بیدرگاه تمام میکند؛ تأییدِ تکراری اعتبارِ دوباره نمیدهد (قیدِ یکتا روی سفارش).
|
||||||
|
> - **کافهبازار:** سفارشِ بازار با `planId` ساخته میشود ( `POST /api/payment/orders` )؛ تا ۱۰ مهر این فیلد نبود و خریدِ بسته از بازار هیچ اعتباری نمیداد (در بازبینی پیدا و با تست بسته شد).
|
||||||
|
> - **لغو و بازپرداخت از پنلِ ادمین:** دو action روی سفارشها؛ بازپرداخت پس از صفحهی تأیید، اعتبارِ خرجنشده و روزهای قلب یا اشتراکِ همان سفارش را پس میگیرد. خودِ پول از پنلِ درگاه برگردانده میشود ( `docs/operations.md` ).
|
||||||
>
|
>
|
||||||
> آزمودهنشده زرینپال درخواستِ پرداخت را با کدِ ۱۰۰ میپذیرد، ولی یک پرداختِ واقعی تا انتها هنوز انجام نشده.
|
> آزمودهنشده زرینپال درخواستِ پرداخت را با کدِ ۱۰۰ میپذیرد، ولی یک پرداختِ واقعی تا انتها هنوز انجام نشده.
|
||||||
|
|
||||||
@@ -337,7 +341,7 @@ with transaction.atomic():
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| ۱ | `GET /api/user/weekly-report` | برای هر یک از ۷ روزِ اخیر و ۷ روزِ قبلش: زمانِ تمرین، سوالِ حلشده و درست، امتیاز، درس و جزیرهی تمامشده، سوال از دستیار. فقط دیتای خودِ صاحبِ توکن. |
|
| ۱ | `GET /api/user/weekly-report` | برای هر یک از ۷ روزِ اخیر و ۷ روزِ قبلش: زمانِ تمرین، سوالِ حلشده و درست، امتیاز، درس و جزیرهی تمامشده، سوال از دستیار. فقط دیتای خودِ صاحبِ توکن. |
|
||||||
| ۲ | — | فرانت جملهی مقایسه («آفرین! این هفته ۲۰ دقیقه بیشتر…») و دو نمودار میسازد: زمانِ هر روز در برابرِ همان روزِ هفتهی پیش، و سوالهای درست/نادرستِ هر روز. |
|
| ۲ | — | فرانت جملهی مقایسه («آفرین! این هفته ۲۰ دقیقه بیشتر…») و دو نمودار میسازد: زمانِ هر روز در برابرِ همان روزِ هفتهی پیش، و سوالهای درست/نادرستِ هر روز. |
|
||||||
| ۳ | `GET /api/progress/nodes?userId=` | فقط شناسهی خودِ کاربر پذیرفته است؛ هر شناسهی دیگری ۴۰۳. |
|
| ۳ | `GET /api/progress/nodes` | پارامترِ `userId` برداشته شد؛ هر کاربر فقط پیشرفتِ خودش را میبیند. |
|
||||||
|
|
||||||
> **تخمینی:** «زمانِ تمرین» اندازهگیری نیست: از فاصلهی جوابها و پرسشها حساب میشود (فاصلهی بیش از ۱۰ دقیقه وقفه است). خواندنِ درس بدونِ جواب دادن در آن نیست، پس عدد معمولاً از زمانِ واقعی کمتر است.
|
> **تخمینی:** «زمانِ تمرین» اندازهگیری نیست: از فاصلهی جوابها و پرسشها حساب میشود (فاصلهی بیش از ۱۰ دقیقه وقفه است). خواندنِ درس بدونِ جواب دادن در آن نیست، پس عدد معمولاً از زمانِ واقعی کمتر است.
|
||||||
|
|
||||||
@@ -382,9 +386,11 @@ with transaction.atomic():
|
|||||||
- `IngestedPage collection, page`
|
- `IngestedPage collection, page`
|
||||||
|
|
||||||
**subscription · payment پول**
|
**subscription · payment پول**
|
||||||
- `Plan price, duration_days, features, badge`
|
- `Plan kind (pack/subscription), price, question_credits, hearts_days, on_sale, ai_daily_quota`
|
||||||
- `Subscription user_id, → Plan, status, expires_at`
|
- `Subscription user_id, → Plan, status, expires_at`
|
||||||
- `Order user_id, plan_id, amount, status, gateway_authority`
|
- `QuestionCredit user_id, delta (+خرید / −سوال / −بازپرداخت), order_id, message_id`
|
||||||
|
- `HeartsPass user_id, expires_at قلبِ نامحدودِ بسته`
|
||||||
|
- `Order user_id, plan_id, amount, list_price, plan_title, status (… canceled, refunded), gateway_authority`
|
||||||
- `Transaction → Order · gateway_ref_id, status, paid_at`
|
- `Transaction → Order · gateway_ref_id, status, paid_at`
|
||||||
- `BazaarCredential توکنِ اتصال به بازار`
|
- `BazaarCredential توکنِ اتصال به بازار`
|
||||||
|
|
||||||
@@ -413,6 +419,7 @@ with transaction.atomic():
|
|||||||
|---|---|
|
|---|---|
|
||||||
| یکتا: `UserAnswer(user_id, question, idempotency_key)` | ثبتِ دوبارهی یک جواب با تکرارِ درخواست |
|
| یکتا: `UserAnswer(user_id, question, idempotency_key)` | ثبتِ دوبارهی یک جواب با تکرارِ درخواست |
|
||||||
| یکتا: `Redemption(کاربر، کد)` | استفادهی دوباره از یک کدِ تخفیف |
|
| یکتا: `Redemption(کاربر، کد)` | استفادهی دوباره از یک کدِ تخفیف |
|
||||||
|
| یکتا: `QuestionCredit(order_id)` برای ردیفِ خرید | اعتبارِ دوباره وقتی تأییدِ پرداخت دو بار برسد |
|
||||||
| یکتا: `CampaignDelivery(کمپین، کاربر، کانال)` | پیامِ تکراری وقتی کمپین دو بار همزمان اجرا شود |
|
| یکتا: `CampaignDelivery(کمپین، کاربر، کانال)` | پیامِ تکراری وقتی کمپین دو بار همزمان اجرا شود |
|
||||||
| یکتا: `LessonChunk(collection, qdrant_point_id)` و `IngestedPage(collection, page)` | تکهی تکراری در Qdrant وقتی خواندنِ کتاب از سر گرفته میشود |
|
| یکتا: `LessonChunk(collection, qdrant_point_id)` و `IngestedPage(collection, page)` | تکهی تکراری در Qdrant وقتی خواندنِ کتاب از سر گرفته میشود |
|
||||||
|
|
||||||
@@ -434,7 +441,7 @@ with transaction.atomic():
|
|||||||
| سقفِ نرخ | هر اندپوینتِ حساس یک `throttle_scope` دارد؛ نرخها همه یکجا در `config/settings.py → DEFAULT_THROTTLE_RATES`. |
|
| سقفِ نرخ | هر اندپوینتِ حساس یک `throttle_scope` دارد؛ نرخها همه یکجا در `config/settings.py → DEFAULT_THROTTLE_RATES`. |
|
||||||
| استریم | `POST /api/ai/ask` با `"stream": true` پاسخ را SSE میفرستد: رویدادهای `chunk`، `done`، `error`. |
|
| استریم | `POST /api/ai/ask` با `"stream": true` پاسخ را SSE میفرستد: رویدادهای `chunk`، `done`، `error`. |
|
||||||
|
|
||||||
### فهرستِ ۶۱ اندپوینت
|
### فهرستِ ۵۹ اندپوینت
|
||||||
|
|
||||||
عمومی بیورود · ورود توکن لازم · داخلی با هدرِ کلیدِ داخلی (سرور-به-سرور)
|
عمومی بیورود · ورود توکن لازم · داخلی با هدرِ کلیدِ داخلی (سرور-به-سرور)
|
||||||
|
|
||||||
@@ -446,7 +453,8 @@ with transaction.atomic():
|
|||||||
| POST | `/api/auth/login` | عمومی | ورود با شماره و کد |
|
| POST | `/api/auth/login` | عمومی | ورود با شماره و کد |
|
||||||
| POST | `/api/auth/refresh` | عمومی | توکنِ تازه با refresh token |
|
| POST | `/api/auth/refresh` | عمومی | توکنِ تازه با refresh token |
|
||||||
| POST | `/api/auth/logout` | ورود | خروج؛ توکنهای قبلی باطل |
|
| POST | `/api/auth/logout` | ورود | خروج؛ توکنهای قبلی باطل |
|
||||||
| GET · PATCH | `/api/user/profile` | ورود | خواندن و ویرایشِ پروفایل |
|
| GET · PATCH | `/api/user/profile` | ورود | خواندن و ویرایشِ پروفایل (تاریخِ تولدِ شمسی، شناسهی نمایشی) |
|
||||||
|
| POST | `/api/user/delete` | ورود | حذفِ حساب؛ شمارهدار با کدِ پیامکیِ تازه، مهمان با تایپِ «حذف» |
|
||||||
| POST | `/api/user/grade` | ورود | ثبتِ پایه |
|
| POST | `/api/user/grade` | ورود | ثبتِ پایه |
|
||||||
| **محتوا و آزمون — `content`** | | | |
|
| **محتوا و آزمون — `content`** | | | |
|
||||||
| GET | `/api/grades` | عمومی | فهرستِ پایهها |
|
| GET | `/api/grades` | عمومی | فهرستِ پایهها |
|
||||||
@@ -459,7 +467,7 @@ with transaction.atomic():
|
|||||||
| POST | `/api/lessons/{id}/complete` | ورود | پایانِ درس |
|
| POST | `/api/lessons/{id}/complete` | ورود | پایانِ درس |
|
||||||
| POST | `/api/questions/{id}/answer` | ورود | ثبتِ جواب |
|
| POST | `/api/questions/{id}/answer` | ورود | ثبتِ جواب |
|
||||||
| POST | `/api/questions/{id}/report` | ورود | گزارشِ ایرادِ سؤال |
|
| POST | `/api/questions/{id}/report` | ورود | گزارشِ ایرادِ سؤال |
|
||||||
| GET | `/api/progress/nodes` | ورود | وضعیتِ کاربر (یا فرزند) روی جزیرهها |
|
| GET | `/api/progress/nodes` | ورود | وضعیتِ خودِ کاربر روی جزیرهها |
|
||||||
| **پیشرفت و بازی — `progress`** | | | |
|
| **پیشرفت و بازی — `progress`** | | | |
|
||||||
| GET | `/api/user/summary` | ورود | پروفایل + آمار + زنجیره در یک درخواست |
|
| GET | `/api/user/summary` | ورود | پروفایل + آمار + زنجیره در یک درخواست |
|
||||||
| GET | `/api/user/hearts` | ورود | قلبها و زمانِ پر شدن |
|
| GET | `/api/user/hearts` | ورود | قلبها و زمانِ پر شدن |
|
||||||
@@ -469,15 +477,16 @@ with transaction.atomic():
|
|||||||
| POST | `/api/characters/select` | ورود | انتخابِ همراه (فقط بازشده) |
|
| POST | `/api/characters/select` | ورود | انتخابِ همراه (فقط بازشده) |
|
||||||
| **دستیار — `ai_tutor` و `rag`** | | | |
|
| **دستیار — `ai_tutor` و `rag`** | | | |
|
||||||
| POST | `/api/ai/ask` | ورود | پرسش؛ JSON یا استریم |
|
| POST | `/api/ai/ask` | ورود | پرسش؛ JSON یا استریم |
|
||||||
|
| GET | `/api/ai/quota` | ورود | سوالهای رایگانِ باقیِ امروز و اعتبارِ بسته |
|
||||||
| GET | `/api/ai/conversations/{id}/messages` | ورود | تاریخچهی گفتگو |
|
| GET | `/api/ai/conversations/{id}/messages` | ورود | تاریخچهی گفتگو |
|
||||||
| POST | `/api/ai/messages/{id}/feedback` | ورود | 👍/👎 روی یک جواب |
|
| POST | `/api/ai/messages/{id}/feedback` | ورود | 👍/👎 روی یک جواب |
|
||||||
| POST | `/api/rag/ingest/text` · `/pdf` | داخلی | افزودنِ متن یا PDF به Qdrant |
|
| POST | `/api/rag/ingest/text` · `/pdf` | داخلی | افزودنِ متن یا PDF به Qdrant |
|
||||||
| GET | `/api/rag/chunks` | داخلی | مرورِ تکههای ذخیرهشده |
|
| GET | `/api/rag/chunks` | داخلی | مرورِ تکههای ذخیرهشده |
|
||||||
| **اشتراک، پرداخت، تخفیف** | | | |
|
| **بسته، پرداخت، تخفیف** | | | |
|
||||||
| GET | `/api/subscription/plans` | عمومی | پلنها |
|
| GET | `/api/subscription/plans` | عمومی | بستههای سوالِ در حالِ فروش |
|
||||||
| POST | `/api/subscription/purchase` | ورود | شروعِ خرید؛ سفارش و لینکِ پرداخت |
|
| POST | `/api/subscription/purchase` | ورود | شروعِ خرید؛ سفارش و لینکِ پرداخت |
|
||||||
| GET | `/api/subscription/status` | ورود | وضعیتِ اشتراک (و بازسنجیِ پرداختِ جاافتاده) |
|
| GET | `/api/subscription/status` | ورود | اعتبارِ سوال، قلبِ نامحدود تا کِی، اشتراکِ قدیمی (و بازسنجیِ پرداختِ جاافتاده) |
|
||||||
| POST | `/api/payment/orders` | ورود | ساختِ سفارش |
|
| POST | `/api/payment/orders` | داخلی | ساختِ سفارش (سفارشِ بازار، با `planId` ِ بسته) |
|
||||||
| GET | `/api/payment/orders/{id}` | ورود | وضعیتِ سفارش |
|
| GET | `/api/payment/orders/{id}` | ورود | وضعیتِ سفارش |
|
||||||
| GET | `/api/payment/invoices` | ورود | پرداختها و فاکتورهای کاربر (شمارهی فاکتور، تخفیف، کدِ پیگیری) |
|
| GET | `/api/payment/invoices` | ورود | پرداختها و فاکتورهای کاربر (شمارهی فاکتور، تخفیف، کدِ پیگیری) |
|
||||||
| GET | `/api/payment/orders/{id}/pay` | عمومی | رفتن به درگاهِ زرینپال |
|
| GET | `/api/payment/orders/{id}/pay` | عمومی | رفتن به درگاهِ زرینپال |
|
||||||
@@ -692,7 +701,19 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
|
|||||||
| انتخابِ مدلِ جواب: ۱۰۲ مسئلهی تصحیحشدهی دستی | `gpt-4.1-mini` : ۹۵–۹۶ درست · `gpt-4o-mini` : ۸۵–۹۰ |
|
| انتخابِ مدلِ جواب: ۱۰۲ مسئلهی تصحیحشدهی دستی | `gpt-4.1-mini` : ۹۵–۹۶ درست · `gpt-4o-mini` : ۸۵–۹۰ |
|
||||||
| شکلها: ۶۴ مسئلهی تصادفیِ کتاب، وارسیِ چشمیِ تکتک | ۴۰ درست، ۱۲ قابلِ قبول، ۷ غلط، ۵ بیشکل — حدودِ یکی از پنج هنوز ایراد دارد؛ برای همین شکل همیشه کنارِ حلِ متنی میآید |
|
| شکلها: ۶۴ مسئلهی تصادفیِ کتاب، وارسیِ چشمیِ تکتک | ۴۰ درست، ۱۲ قابلِ قبول، ۷ غلط، ۵ بیشکل — حدودِ یکی از پنج هنوز ایراد دارد؛ برای همین شکل همیشه کنارِ حلِ متنی میآید |
|
||||||
| سرعت | جوابِ کامل با شکل ~۳۰ ثانیه؛ اندازهگیری شد که در خودِ GapGPT است («فقط بگو سلام» = ۷ ثانیه) |
|
| سرعت | جوابِ کامل با شکل ~۳۰ ثانیه؛ اندازهگیری شد که در خودِ GapGPT است («فقط بگو سلام» = ۷ ثانیه) |
|
||||||
| سقفها | ۱۵ سؤال در روز برای هر کاربر؛ ۲۰ در ساعت؛ مهمانها بر اساسِ IP (تا با ساختِ حسابِ مهمانِ تازه دور زده نشود) |
|
| سقفها | ۳ سؤالِ رایگان در روز، بعد از اعتبارِ بسته؛ ۲۰ در ساعت؛ مهمانها بر اساسِ IP (تا با ساختِ حسابِ مهمانِ تازه دور زده نشود) |
|
||||||
|
|
||||||
|
### ۹-۷ فقط ریاضی، و چه چیزی هزینه دارد
|
||||||
|
|
||||||
|
دستیار فقط دربارهی ریاضی حرف میزند (`rag/services/guardrails.py`). سه لایه دارد: (۱) صافیِ سرور پیش از مدل و بیهزینه، فقط برای ترفندهای آشکار («دستورهای قبلی را نادیده بگیر»، «پرامپتت را بگو»)؛ (۲) قاعدهی سخت در پرامپت که هر چیزِ غیرِ ریاضی را با علامتی ثابت جواب میدهد؛ (۳) سرور آن علامت را با متنِ ثابتِ رد عوض میکند. جملهای دربارهی آسیب به خود رد نمیشود و جوابی مهربان با شمارهی اورژانسِ اجتماعی (۱۲۳) و صدای مشاور (۱۴۸۰) میگیرد؛ از ۱۰ مهر هر شکلِ نوشتنش (با فاصله، بینیمفاصله) گرفته میشود. آزمونِ زنده: ۷ از ۷ تلاشِ خروج از ریاضی رد شد.
|
||||||
|
|
||||||
|
| قاعدهی خرج | |
|
||||||
|
|---|---|
|
||||||
|
| ترتیب | اول ۳ سوالِ رایگانِ همان روز، بعد اعتبارِ بسته ( `Message.charge` : `free` / `credit` / `none` ) |
|
||||||
|
| خرج میشود | هر سوالی که به مدل برسد، از جمله ردِ «غیرِ ریاضی» توسطِ مدل |
|
||||||
|
| خرج نمیشود | ردِ صافیِ سرور، جوابِ کششده، جوابی که ساخته نشد، و تکرارِ درخواست با همان `Idempotency-Key` |
|
||||||
|
| همزمانی | دو سوالِ همزمان با یک اعتبار فقط یک جواب میگیرند؛ با دو thread واقعی آزموده میشود ( `ai_tutor/tests_credit_concurrency.py` ) |
|
||||||
|
| هزینه | هر سوال حدودِ ۰٫۲ سنت؛ قیمتِ اندازهگیریشدهی هر مدل و تاریخچهاش در `docs/model-prices.md` |
|
||||||
|
|
||||||
## ۱۰. محتوای آموزشی و ساختِ سؤال
|
## ۱۰. محتوای آموزشی و ساختِ سؤال
|
||||||
|
|
||||||
@@ -758,17 +779,21 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
|
|||||||
| امتیاز (XP) | پایانِ درس یا جزیره: **۱۰ × جوابِ درست**، فقط بارِ اول. عددِ جوابِ درستی که اپ میفرستد به جوابهای درستِ **ثبتشده** محدود میشود؛ صدا زدنِ مستقیمِ API امتیازِ مجانی نمیدهد. | `content/views.py → NodeQuizCompleteView` |
|
| امتیاز (XP) | پایانِ درس یا جزیره: **۱۰ × جوابِ درست**، فقط بارِ اول. عددِ جوابِ درستی که اپ میفرستد به جوابهای درستِ **ثبتشده** محدود میشود؛ صدا زدنِ مستقیمِ API امتیازِ مجانی نمیدهد. | `content/views.py → NodeQuizCompleteView` |
|
||||||
| ستاره | ۹۰٪ به بالا ۳ · ۶۰٪ به بالا ۲ · بیشتر از صفر ۱ | همان |
|
| ستاره | ۹۰٪ به بالا ۳ · ۶۰٪ به بالا ۲ · بیشتر از صفر ۱ | همان |
|
||||||
| سکه | **۵ × ستاره**، فقط بارِ اول | همان |
|
| سکه | **۵ × ستاره**، فقط بارِ اول | همان |
|
||||||
| قلب | ۵ قلب؛ هر جوابِ غلط یکی کم؛ هر **۵ ساعت** یکی برمیگردد و **۲۴ ساعت** بعد از اولین قلبِ ازدسترفته همه پر میشوند. مشترکِ فعال قلب از دست نمیدهد. | `progress/models.py → UserHearts` |
|
| قلب | ۵ قلب؛ هر جوابِ غلط یکی کم؛ هر **۵ ساعت** یکی برمیگردد و **۲۴ ساعت** بعد از اولین قلبِ ازدسترفته همه پر میشوند. با قلبِ صفر نه جوابی پذیرفته میشود نه پایانِ درس یا جزیرهی بیجواب ( `403 no_hearts` ). اشتراکِ فعال یا روزهای قلبِ بسته = قلبِ نامحدود. | `progress/models.py → UserHearts` · `content/views.py` |
|
||||||
| زنجیره (Streak) | جواب، پایانِ درس یا جزیره امروز را (به وقتِ تهران) روزِ فعال میکند. اگر آخرین روزِ فعال نه امروز باشد نه دیروز، صفر نشان داده میشود. | `progress/services.py` |
|
| زنجیره (Streak) | جواب، پایانِ درس یا جزیره امروز را (به وقتِ تهران) روزِ فعال میکند. اگر آخرین روزِ فعال نه امروز باشد نه دیروز، صفر نشان داده میشود. | `progress/services.py` |
|
||||||
| جدولِ رتبه | هفتگی (امتیازِ ۷ روزِ اخیر) و همهی زمانها؛ زنده حساب و ۶۰ ثانیه کش میشود. | `progress/leaderboard.py` |
|
| جدولِ رتبه | هفتگی (امتیازِ ۷ روزِ اخیر) و همهی زمانها؛ زنده حساب و ۶۰ ثانیه کش میشود. | `progress/leaderboard.py` |
|
||||||
| پیشرفت روی نقشه | پایانِ آزمون، جزیره را با هر نمرهای تمام میکند و جزیرهی بعدی ( `nextNodeId` ) باز میشود؛ نمره فقط ستاره و امتیاز را تعیین میکند (تصمیمِ محصولی: ساده بماند). | `content/services.py` |
|
| پیشرفت روی نقشه | پایانِ آزمون، جزیره را با هر نمرهای تمام میکند و جزیرهی بعدی ( `nextNodeId` ) باز میشود؛ نمره فقط ستاره و امتیاز را تعیین میکند (تصمیمِ محصولی: ساده بماند). | `content/services.py` |
|
||||||
| اعلان | «جزیره را با ۳ ستاره تمام کردی و ۴۰ امتیاز گرفتی» — با رقمِ فارسی. | `notifications/services.py` |
|
| اعلان | «جزیره را با ۳ ستاره تمام کردی و ۴۰ امتیاز گرفتی» — با رقمِ فارسی. | `notifications/services.py` |
|
||||||
|
|
||||||
> **محدودیتِ قلب در سرور فعلاً خاموش است** ( `HEARTS_ENFORCED=0` ): با صفر قلب، اپ جلوی کاربر را میگیرد ولی API هنوز جواب را میپذیرد، چون فرانت صفحهی «قلبها تمام شد» را ندارد. روشن کردنش یک خط در `.env` است. تنها مزیتِ واقعیِ اشتراک در کد فعلاً همین «قلبِ نامحدود» است — یک تصمیمِ محصولی.
|
> **قلب واقعی است** ( `HEARTS_ENFORCED` پیشفرض روشن): قلبِ صفر بازی را متوقف میکند تا قلبِ بعدی برگردد. چکِ قلب داخلِ همان تراکنشی است که قلب را کم میکند، پس دو جوابِ همزمان با یک قلبِ باقیمانده هر دو پذیرفته نمیشوند؛ و پایانِ آزمون بیجواب دادن هم بسته است. کسی که آخرین قلبش را روی آخرین سوال از دست داده، جزیره را تمام میکند. تستها: `content/tests_hearts_gate.py` .
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*شکلِ ۱۱-۳ — صفحهی «قلبهات تموم شد»: شمارشِ معکوسِ زنده تا قلبِ بعدی، قلبی که کمکم پر میشود، ساعتِ پر شدنِ همه، پیشنهادِ بستهها و «تا اون موقع از پی بپرس» (پرسیدن قلب خرج نمیکند). وقتی وقتش رسید، صفحه خودش از سرور میپرسد و «ادامه بده» را نشان میدهد. فرانت: `src/components/NoHeartsScreen.tsx` .*
|
||||||
|
|
||||||
## ۱۲. امنیت و احرازِ هویت
|
## ۱۲. امنیت و احرازِ هویت
|
||||||
|
|
||||||
دو دور بازبینیِ بدبینانه روی کلِ بکاند انجام شد (مرداد و ۳ مهر)، و یک وارسیِ نهایی روی سایتِ زنده (۷ مهر) با این سؤال: «یک دانشآموزِ متقلب، یک مهاجم یا یک والدِ سردرگم چطور این را میشکند؟» هر یافته با یک تست بسته شد. این بخش صادقانه میگوید چه بسته است و چه هنوز باز.
|
سه دور بازبینیِ بدبینانه روی کلِ بکاند انجام شد (مرداد، ۳ مهر و ۱۰ مهر)، و یک وارسیِ نهایی روی سایتِ زنده (۷ مهر) با این سؤال: «یک دانشآموزِ متقلب، یک مهاجم یا یک والدِ سردرگم چطور این را میشکند؟» هر یافته با یک تست بسته شد. این بخش صادقانه میگوید چه بسته است و چه هنوز باز.
|
||||||
|
|
||||||
### ۱۲-۱ هویت و توکن
|
### ۱۲-۱ هویت و توکن
|
||||||
|
|
||||||
@@ -783,14 +808,15 @@ if (Math.sign(y) !== Math.sign(py)) { const r = bisect(f, px, x); if (Math.abs(f
|
|||||||
|
|
||||||
| حوزه | چه کردهایم | کجا |
|
| حوزه | چه کردهایم | کجا |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| دسترسی به دادهی دیگران | شناسهی کاربر همیشه از توکن خوانده میشود، هرگز از ورودی؛ دیدنِ پیشرفتِ فرزند فقط با اتصالِ تأییدشده. | همهی `views.py` ها |
|
| دسترسی به دادهی دیگران | شناسهی کاربر همیشه از توکن خوانده میشود، هرگز از ورودی؛ هر کاربر فقط دادهی خودش را میبیند (گزارشِ والدین هم در حسابِ خودِ دانشآموز است). | همهی `views.py` ها |
|
||||||
| سقفِ نرخ | روی همهی نقاطِ حساس (ورود، دستیار، خرید، کدِ تخفیف، جواب…)؛ مهمان بر اساسِ IPِ واقعی ( `NUM_PROXIES=2` : آروان + nginx). | `config/settings.py` |
|
| سقفِ نرخ | روی همهی نقاطِ حساس (ورود، دستیار، خرید، کدِ تخفیف، جواب…)؛ مهمان بر اساسِ IPِ واقعی ( `NUM_PROXIES=2` : آروان + nginx). | `config/settings.py` |
|
||||||
| پیامک و هزینهاش | شماره به `09XXXXXXXXX` نرمال میشود؛ فاصله بینِ دو درخواست، ۱۰ در روز برای هر شماره، ۳۰۰ در روز برای کلِ سامانه (ضدِ SMS pumping) — شمارش در دیتابیس، نه حافظهی موقت. | `accounts/phone.py` · `tests_otp_limits.py` |
|
| پیامک و هزینهاش | شماره به `09XXXXXXXXX` نرمال میشود؛ فاصله بینِ دو درخواست، ۱۰ در روز برای هر شماره، ۳۰۰ در روز برای کلِ سامانه (ضدِ SMS pumping) — شمارش در دیتابیس، نه حافظهی موقت. | `accounts/phone.py` · `tests_otp_limits.py` |
|
||||||
| پرداخت | فقط تأییدِ سرور-به-سرور؛ برگشتِ جعلی اثری ندارد؛ webhookِ عمومیِ ناامن حذف شد؛ هر درگاهی بیکلید «بسته» میماند (fail-closed)، نه موفقیتِ ساختگی. | `payment/` |
|
| پرداخت | فقط تأییدِ سرور-به-سرور؛ برگشتِ جعلی اثری ندارد؛ webhookِ عمومیِ ناامن حذف شد؛ هر درگاهی بیکلید «بسته» میماند (fail-closed)، نه موفقیتِ ساختگی. اعتبارِ بسته دفتری است و هر سفارش فقط یک بار اعتبار میدهد؛ وضعیتِ سفارش در ادمین فقط با actionِ لغو/بازپرداخت عوض میشود. | `payment/` · `subscription/` |
|
||||||
| تقلبِ امتیاز | امتیاز فقط برای جوابِ ثبتشده؛ جایزه فقط بارِ اول. | `content/views.py` |
|
| تقلبِ امتیاز | امتیاز فقط برای جوابِ ثبتشده؛ جایزه فقط بارِ اول. | `content/views.py` |
|
||||||
| درخواستِ تکراری و همزمانی | Idempotency-Key در شش نقطه؛ `transaction.atomic` ؛ قیدِ یکتا در دیتابیس؛ SQLite در حالتِ `IMMEDIATE` تا قفلها واقعاً قفل کنند. | `config/settings.py` |
|
| درخواستِ تکراری و همزمانی | Idempotency-Key در شش نقطه؛ `transaction.atomic` ؛ قیدِ یکتا در دیتابیس؛ SQLite در حالتِ `IMMEDIATE` تا قفلها واقعاً قفل کنند. | `config/settings.py` |
|
||||||
| خطا و لاگ | هر خطا JSONِ یکدست بیجزئیاتِ داخلی؛ لاگ بدونِ بدنه، هدر، توکن یا رمز. | `config/exceptions.py` · `middleware.py` |
|
| خطا و لاگ | هر خطا JSONِ یکدست بیجزئیاتِ داخلی؛ لاگ بدونِ بدنه، هدر، توکن یا رمز. | `config/exceptions.py` · `middleware.py` |
|
||||||
| محتوای مدل | HTML یا SVGِ تولیدِ مدل هرگز واردِ صفحه نمیشود؛ فقط JSONِ وارسیشده با فهرستِ سفید. | `rag/services/diagram.py` |
|
| محتوای مدل | HTML یا SVGِ تولیدِ مدل هرگز واردِ صفحه نمیشود؛ فقط JSONِ وارسیشده با فهرستِ سفید. دستیار فقط ریاضی (بخشِ ۹-۷). | `rag/services/diagram.py` · `guardrails.py` |
|
||||||
|
| حذفِ حساب | شمارهدار با کدِ پیامکیِ تازه (همان سقفِ حدسِ ورود)، مهمان با تایپِ «حذف»؛ حسابِ نمایشی حذف نمیشود؛ سفارشها برای حسابداری میمانند؛ توکنِ قبلی بعد از حذف کار نمیکند. | `accounts/views.py → DeleteAccountView` |
|
||||||
| مرورگر | CORS فقط برای دامنهی فرانت؛ پاسخِ API با `no-store` در کشِ آروان نمیماند؛ نامِ نمایشی از شماره و لینک پاک میشود؛ سقفِ حدسِ رمزِ ادمین برای هر IP. | `config/middleware.py` |
|
| مرورگر | CORS فقط برای دامنهی فرانت؛ پاسخِ API با `no-store` در کشِ آروان نمیماند؛ نامِ نمایشی از شماره و لینک پاک میشود؛ سقفِ حدسِ رمزِ ادمین برای هر IP. | `config/middleware.py` |
|
||||||
| کلیدها | همه در `.env` ِ سرور؛ هیچ کلیدی در git یا در پاسخِ API نیست؛ `/api/health` فقط «بله/خیر» میگوید. | `.env.example` |
|
| کلیدها | همه در `.env` ِ سرور؛ هیچ کلیدی در git یا در پاسخِ API نیست؛ `/api/health` فقط «بله/خیر» میگوید. | `.env.example` |
|
||||||
|
|
||||||
@@ -855,7 +881,8 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
|||||||
| `KAVENEGAR_API_KEY` · `KAVENEGAR_OTP_TEMPLATE` | درخواستِ کد ۵۰۳ صریح میگیرد. الگو باید `SMSVerify` بماند. |
|
| `KAVENEGAR_API_KEY` · `KAVENEGAR_OTP_TEMPLATE` | درخواستِ کد ۵۰۳ صریح میگیرد. الگو باید `SMSVerify` بماند. |
|
||||||
| `ZARINPAL_MERCHANT_ID` · `BAZAAR_*` | پرداخت بسته میماند (۵۰۳). |
|
| `ZARINPAL_MERCHANT_ID` · `BAZAAR_*` | پرداخت بسته میماند (۵۰۳). |
|
||||||
| `CACHE_URL` | کشِ جدا برای هر worker؛ `/api/health` هشدار میدهد. |
|
| `CACHE_URL` | کشِ جدا برای هر worker؛ `/api/health` هشدار میدهد. |
|
||||||
| `HEARTS_ENFORCED` · `TEST_LOGIN_PHONES` | خاموش · هیچ حسابِ آزمایشی. |
|
| `HEARTS_ENFORCED` · `TEST_LOGIN_PHONES` | روشن (قلب واقعی؛ `0` خاموشش میکند) · هیچ حسابِ آزمایشی. |
|
||||||
|
| `AI_DAILY_QUOTA_FREE` | ۳ سوالِ رایگان در روز. |
|
||||||
| `DJANGO_DEBUG` | خاموش — و **هرگز روی سرور روشن نشود** (کدِ ورودِ همگانیِ `0000` را فعال میکند). |
|
| `DJANGO_DEBUG` | خاموش — و **هرگز روی سرور روشن نشود** (کدِ ورودِ همگانیِ `0000` را فعال میکند). |
|
||||||
|
|
||||||
مقدارهای `.env` با پیشفرضهای کد ادغام میشوند؛ یک `.env` ِ ناقص سرویس را نمیخواباند. شرحِ کامل در `.env.example` و `docs/operations.md`.
|
مقدارهای `.env` با پیشفرضهای کد ادغام میشوند؛ یک `.env` ِ ناقص سرویس را نمیخواباند. شرحِ کامل در `.env.example` و `docs/operations.md`.
|
||||||
@@ -885,10 +912,10 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
|||||||
|
|
||||||
| ویژگی | چرا کارِ همزمان را ممکن میکند |
|
| ویژگی | چرا کارِ همزمان را ممکن میکند |
|
||||||
|---|---|
|
|---|---|
|
||||||
| ۱۵ اپ با مرزِ روشن | هر نفر صاحبِ چند اپ میشود (مثلاً یکی پرداخت و اشتراک، یکی محتوا و گیمیفیکیشن، یکی دستیار)؛ تغییرِ هر کس کمتر به فایلِ دیگری میخورد. |
|
| ۱۴ اپ با مرزِ روشن | هر نفر صاحبِ چند اپ میشود (مثلاً یکی پرداخت و اشتراک، یکی محتوا و گیمیفیکیشن، یکی دستیار)؛ تغییرِ هر کس کمتر به فایلِ دیگری میخورد. |
|
||||||
| بیFK بینِ اپها | تغییرِ جدولهای یک اپ migrationِ اپِ دیگر را نمیشکند؛ هر اپ بعداً قابلِ جدا شدن به سرویسِ مستقل است. |
|
| بیFK بینِ اپها | تغییرِ جدولهای یک اپ migrationِ اپِ دیگر را نمیشکند؛ هر اپ بعداً قابلِ جدا شدن به سرویسِ مستقل است. |
|
||||||
| قراردادِ OpenAPI | فرانت و بکاند بیجلسه روی یک شکلِ داده توافق دارند؛ `tests_frontend_contract.py` تغییرِ ناخواسته را میگیرد. |
|
| قراردادِ OpenAPI | فرانت و بکاند بیجلسه روی یک شکلِ داده توافق دارند؛ `tests_frontend_contract.py` تغییرِ ناخواسته را میگیرد. |
|
||||||
| ۶۸۵ تستِ خودکار | تازهوارد بیترس تغییر میدهد: اگر چیزی را شکست، تست قبل از سرور خبر میدهد. |
|
| ۷۷۹ تستِ خودکار | تازهوارد بیترس تغییر میدهد: اگر چیزی را شکست، تست قبل از سرور خبر میدهد. |
|
||||||
| قواعدِ یکجا | نرخها، دامنهها و تنظیمات فقط در `config/settings.py` ؛ پیامِ خطا، لاگ و هویت یک مسیرِ مشترک دارند. |
|
| قواعدِ یکجا | نرخها، دامنهها و تنظیمات فقط در `config/settings.py` ؛ پیامِ خطا، لاگ و هویت یک مسیرِ مشترک دارند. |
|
||||||
| مستندات در مخزن | `docs/` + همین سند + Swagger + توضیحِ «چرا» کنارِ کدِ حساس. |
|
| مستندات در مخزن | `docs/` + همین سند + Swagger + توضیحِ «چرا» کنارِ کدِ حساس. |
|
||||||
|
|
||||||
@@ -921,7 +948,7 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
|||||||
|
|
||||||
| چه | چطور |
|
| چه | چطور |
|
||||||
|---|---|
|
|---|---|
|
||||||
| تستِ خودکارِ بکاند | **۶۸۵ تست**، همه سبز ( `python manage.py test` ). روی فایلِ دیتابیسِ واقعی ( `.test-db.sqlite3` ) اجرا میشوند تا رفتارِ قفلها مثلِ سرور باشد. هر باگِ پیداشده در بازبینیها یک تستِ اختصاصی دارد ( `tests_audit_fixes.py`، `tests_otp_limits.py`، …). |
|
| تستِ خودکارِ بکاند | **۷۷۹ تست**، همه سبز ( `python manage.py test` ). روی فایلِ دیتابیسِ واقعی ( `.test-db.sqlite3` ) اجرا میشوند تا رفتارِ قفلها مثلِ سرور باشد. هر باگِ پیداشده در بازبینیها یک تستِ اختصاصی دارد ( `tests_audit_fixes.py`، `tests_otp_limits.py`، `tests_hearts_gate.py`، …). همزمانی با دو thread واقعی آزموده میشود (اعتبارِ دستیار، آخرین قلب). |
|
||||||
| قرارداد با فرانت | `content/tests_frontend_contract.py` و build با TypeScriptِ strict در فرانت. |
|
| قرارداد با فرانت | `content/tests_frontend_contract.py` و build با TypeScriptِ strict در فرانت. |
|
||||||
| تستِ روی سایتِ زنده | هر صفحه با حسابهای نمایشی روی سرورِ واقعی؛ مرورگرِ خودکار (puppeteer) برای وارسیِ صفحهها. |
|
| تستِ روی سایتِ زنده | هر صفحه با حسابهای نمایشی روی سرورِ واقعی؛ مرورگرِ خودکار (puppeteer) برای وارسیِ صفحهها. |
|
||||||
| کیفیتِ دستیار | `rag_model_benchmark` (مسئلههای واقعیِ کتاب از مسیرِ واقعی، با هزینهی واقعی)، `ai_feedback_report` (👍/👎 به تفکیکِ نسخهی پرامپت)، وارسیِ چشمیِ نمونه پیش از اعتماد به هر اجرای انبوه. |
|
| کیفیتِ دستیار | `rag_model_benchmark` (مسئلههای واقعیِ کتاب از مسیرِ واقعی، با هزینهی واقعی)، `ai_feedback_report` (👍/👎 به تفکیکِ نسخهی پرامپت)، وارسیِ چشمیِ نمونه پیش از اعتماد به هر اجرای انبوه. |
|
||||||
@@ -932,7 +959,7 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
|||||||
|
|
||||||
## ۱۶. وضعیتِ فعلی، محدودیتها و تصمیمهای باز
|
## ۱۶. وضعیتِ فعلی، محدودیتها و تصمیمهای باز
|
||||||
|
|
||||||
### وضعیت در تاریخِ نگارشِ سند (۷ مهر ۱۴۰۵، روی سرورِ زنده)
|
### وضعیت در تاریخِ نگارشِ سند (۱۰ مهر ۱۴۰۵، روی سرورِ زنده)
|
||||||
|
|
||||||
| بخش | وضعیت | توضیح |
|
| بخش | وضعیت | توضیح |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -941,7 +968,8 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
|||||||
| پیامکِ ورود | کار میکند | کدِ واقعی به گوشی رسید (الگوی SMSVerify)؛ هر پیامک هزینه دارد |
|
| پیامکِ ورود | کار میکند | کدِ واقعی به گوشی رسید (الگوی SMSVerify)؛ هر پیامک هزینه دارد |
|
||||||
| دستیار + شکل | کار میکند | کُند (~۳۰ ثانیه، از سمتِ GapGPT)؛ ~یک شکل از پنج ایراد دارد |
|
| دستیار + شکل | کار میکند | کُند (~۳۰ ثانیه، از سمتِ GapGPT)؛ ~یک شکل از پنج ایراد دارد |
|
||||||
| زرینپال | نیمه | درخواستِ پرداخت پذیرفته میشود؛ پرداختِ واقعی تا انتها آزموده نشده |
|
| زرینپال | نیمه | درخواستِ پرداخت پذیرفته میشود؛ پرداختِ واقعی تا انتها آزموده نشده |
|
||||||
| کافهبازار | منتظر | کد آماده؛ منتظرِ تأییدِ حسابِ شرکت و اپِ اندرویدی |
|
| کافهبازار | منتظر | اتصالِ API برقرار؛ منتظرِ تعریفِ بستهها در پنلِ بازار (محصولِ مصرفی) و اپِ اندرویدی |
|
||||||
|
| بستهی سوال و قلب | زنده | سه بسته روی سایت؛ قلبِ صفر بازی را متوقف میکند |
|
||||||
| پیامِ گروهی | پایه | ذخیره و هدفگیری هست؛ زمانبندیِ خودکار نه |
|
| پیامِ گروهی | پایه | ذخیره و هدفگیری هست؛ زمانبندیِ خودکار نه |
|
||||||
| بکاپِ خودکار | ندارد | دستی پیش از هر انتشار |
|
| بکاپِ خودکار | ندارد | دستی پیش از هر انتشار |
|
||||||
|
|
||||||
@@ -950,8 +978,9 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
|||||||
| موضوع | چرا تصمیم لازم است |
|
| موضوع | چرا تصمیم لازم است |
|
||||||
|---|---|
|
|---|---|
|
||||||
| عوض کردنِ کلیدِ امضا | کد آماده؛ همه یک بار از حساب بیرون میافتند — زمانش را باید تعیین کرد. |
|
| عوض کردنِ کلیدِ امضا | کد آماده؛ همه یک بار از حساب بیرون میافتند — زمانش را باید تعیین کرد. |
|
||||||
| پاکسازیِ دادهی دمو | پیش از اولین کاربرِ واقعی: ۷ شمارهی آزمایشی، حسابهای دمو و مهمانهای تستی. |
|
| پاکسازیِ دادهی دمو | حسابهای آزمایشیِ شمارهدار ۱۰ مهر پاک شدند (با بکاپ)؛ ۷ شمارهی نمایشی و حسابهایشان عمداً ماندهاند. |
|
||||||
| مزیتِ اشتراک | در حالِ حاضر فقط «قلبِ نامحدود»؛ دستیار و درسها برای همه آزادند. چه چیزی پولی باشد؟ |
|
| قیمتِ بستهها و سهمیهی رایگان | بعد از دو هفته مصرفِ واقعی بازبینی شود؛ سود در `docs/ai-cost-and-guardrails.md` حساب شده. |
|
||||||
|
| سیاستِ بازپرداخت | الان فقط اعتبارِ خرجنشده پس گرفته میشود؛ پول از پنلِ درگاه. |
|
||||||
| Redis و Postgres | هماهنگی با ادمینِ سرور؛ هر دو روی همان ماشین موجودند. |
|
| Redis و Postgres | هماهنگی با ادمینِ سرور؛ هر دو روی همان ماشین موجودند. |
|
||||||
| پورتِ ۸۰ و دسترسیِ SSH | کارِ ادمینِ سرور. |
|
| پورتِ ۸۰ و دسترسیِ SSH | کارِ ادمینِ سرور. |
|
||||||
| پیشرفت با هر نمره | در حالِ حاضر جزیره با هر نمرهای تمام میشود؛ اگر حدِ قبولی لازم است، یک تصمیمِ آموزشی است. |
|
| پیشرفت با هر نمره | در حالِ حاضر جزیره با هر نمرهای تمام میشود؛ اگر حدِ قبولی لازم است، یک تصمیمِ آموزشی است. |
|
||||||
@@ -963,7 +992,7 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
|||||||
3. یک پرداختِ واقعیِ کامل با زرینپال.
|
3. یک پرداختِ واقعیِ کامل با زرینپال.
|
||||||
4. `CACHE_URL` (Redis) و بعد Postgres.
|
4. `CACHE_URL` (Redis) و بعد Postgres.
|
||||||
5. صفِ کار برای دستیار؛ سنجشِ شکل روی نمونهی بزرگتر.
|
5. صفِ کار برای دستیار؛ سنجشِ شکل روی نمونهی بزرگتر.
|
||||||
6. روشن کردنِ `HEARTS_ENFORCED` همراه با صفحهی «قلبها تمام شد» در فرانت.
|
6. تعریفِ سه بسته در پنلِ کافهبازار و اپِ اندرویدی با Poolakey.
|
||||||
|
|
||||||
## پیوست
|
## پیوست
|
||||||
|
|
||||||
@@ -1022,6 +1051,8 @@ npm run build # type-check + build در dist/
|
|||||||
| `docs/security-before-production.md` | ۱۸ موردِ امنیتیِ باز با خطر و راهِ بستن |
|
| `docs/security-before-production.md` | ۱۸ موردِ امنیتیِ باز با خطر و راهِ بستن |
|
||||||
| `docs/content-authoring.md` | ویرایشِ محتوا و ساختِ انبوهِ سؤال |
|
| `docs/content-authoring.md` | ویرایشِ محتوا و ساختِ انبوهِ سؤال |
|
||||||
| `docs/extraction-playbook.md` | خواندنِ کتاب با مدلِ بینایی: هزینه و درسها |
|
| `docs/extraction-playbook.md` | خواندنِ کتاب با مدلِ بینایی: هزینه و درسها |
|
||||||
|
| `docs/ai-cost-and-guardrails.md` | هزینهی هر سوالِ دستیار، بستهها و قاعدهی خرج، محافظِ «فقط ریاضی» |
|
||||||
|
| `docs/model-prices.md` | دفترِ قیمتِ اندازهگیریشدهی مدلها، با تاریخ؛ اگر قیمت عوض شد ردیف اضافه شود |
|
||||||
| `data/textbooks/README.md` | افزودنِ کتابِ تازه |
|
| `data/textbooks/README.md` | افزودنِ کتابِ تازه |
|
||||||
| `/api/docs/` | قراردادِ زنده و قابلِ امتحانِ API |
|
| `/api/docs/` | قراردادِ زنده و قابلِ امتحانِ API |
|
||||||
|
|
||||||
|
|||||||