tutor answers step by step in plain text, islands finish, and the audit's money and login holes are closed
Tutor (prompt tutor-v3-stepwise-plain): plain text instead of LaTeX/markdown, with rag/services/mathtext.py converting whatever still slips through; full step-by-step solutions with a check before the final answer; three tones by grade; the last 6 messages of the conversation go to the model and follow-ups are no longer served from another user's cache; book solutions the checker disputes are kept out of the context; a questionId gives the tutor the practice question and the student's wrong answer. /api/health reports when the AI credit runs out. rag_model_benchmark compares chat models on verified book problems (run on the server; credit ran out mid-run). Map: only lessons that have questions count toward finishing an island, lessonIds[0] / nextLessonId is always the next lesson to play, and a finished island stays finished. The Seyar characters (sigma/delta/pi) are seeded. Audit fixes, each reproduced before fixing: - the plan is stored on the order, so paying a monthly order after starting a yearly checkout no longer grants a year - opening checkout no longer demotes an active subscription; renewals extend from the current expiry; expired subscriptions are not reported active - one discount code per order and only before the gateway; a 100% code completes the purchase; lost Zarinpal confirmations are re-verified - the unused /api/payment/webhook "mark paid" endpoint is removed - XP counts only answers recorded on the server - 6-digit OTP, each code dies after 5 wrong guesses; POST /api/auth/logout - a guest who signs up keeps their account and progress (test phones excluded) - affiliate commission on the first real payment; guests excluded - parents with several children no longer get a 500 - SQLite runs transactions IMMEDIATE, since it ignores select_for_update - admin login is rate-limited; display names drop phone numbers and links; userQuestion is capped at 2000 characters - server-side heart enforcement exists behind HEARTS_ENFORCED (off until the frontend handles 403 no_hearts) Not done on purpose: rotating the server's secret key and moving to Postgres. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,27 @@
|
||||
`DJANGO_SECRET_KEY` همین توکنها را امضا میکند. عوض شدنش یعنی **باطل شدنِ
|
||||
همهی نشستهای فعال**.
|
||||
|
||||
**توکنِ access فقط `user_id`ِ امضاشده است و به `Session` وصل نیست.** خروج
|
||||
(`POST /api/auth/logout`) refreshِ همان دستگاه را باطل میکند و
|
||||
`User.tokens_valid_after` را میگذارد؛ هر توکنِ access که پیش از آن لحظه صادر شده
|
||||
رد میشود (دقتِ ثانیهای). دستگاههای دیگر با refreshِ خودشان بیصدا توکنِ تازه
|
||||
میگیرند. پیش از ۲۰۲۶-۰۹-۲۵ خروج وجود نداشت.
|
||||
|
||||
**کدِ ورود ۶ رقمی است و هر کد بعد از ۵ حدسِ غلط باطل میشود**
|
||||
(`OTPCode.failed_attempts`، شمارش در دیتابیس، نه در کشِ per-worker).
|
||||
|
||||
**مهمانی که با شماره وارد میشود همان کاربر میماند.** اگر درخواستِ ورود توکنِ
|
||||
مهمان را همراه دارد و آن شماره هنوز حسابی ندارد، به همان ردیفِ مهمان شماره داده
|
||||
میشود؛ امتیاز، استریک، جزیرهها و گفتگوها سرِ جایشان میمانند. شمارهای که از قبل
|
||||
حساب دارد ادغام نمیشود (حسابِ موجود باز میشود و مهمان دستنخورده میماند)، و
|
||||
`TEST_LOGIN_PHONES` هرگز مهمانی را جذب نمیکنند تا حسابهای دمو عوض نشوند.
|
||||
|
||||
**نامِ نمایشی** در جدولِ رتبهبندی به بچههای دیگر نشان داده میشود؛ شماره، لینک
|
||||
و آیدی هنگامِ ذخیره از آن حذف میشود (`accounts.serializers.clean_display_name`).
|
||||
|
||||
پنلِ `/admin/` برای هر IP در ۱۵ دقیقه حداکثر `ADMIN_LOGIN_MAX_ATTEMPTS` (۱۰) تلاشِ
|
||||
ورود میپذیرد (`config.middleware.AdminLoginThrottleMiddleware`).
|
||||
|
||||
## اپها با قرارداد جدا شدهاند، نه با ForeignKey
|
||||
|
||||
هر اپ یک مسئولیت دارد و مدلهای خودش را نگه میدارد. ارجاعِ بینِ اپها با
|
||||
@@ -61,6 +82,30 @@ PaymentRequest را میزند، `Order.gateway_authority` را ذخیره م
|
||||
**fail-closed** میشوند (`503` یا نامعتبر)، نه موفقیتِ ساختگی. همین قاعده برای
|
||||
پیامک هم برقرار است.
|
||||
|
||||
قاعدههایی که بازبینیِ ۲۰۲۶-۰۹-۲۵ اضافه کرد (هرکدام با تست در
|
||||
`payment/tests_audit_fixes.py`):
|
||||
|
||||
| قاعده | چون پیشتر… |
|
||||
|---|---|
|
||||
| **پلن روی سفارش است** (`Order.plan_id`) و فعالسازی از همان خوانده میشود | پلن روی Subscription بود؛ شروعِ خریدِ سالانه و پرداختِ سفارشِ ماهانه، اشتراکِ سالانه میداد |
|
||||
| **باز کردنِ صفحهی خرید اشتراکِ فعال را دست نمیزند** | وضعیت همان لحظه `pending` میشد و کاربرِ پولی مزیتش را از دست میداد |
|
||||
| **تمدید از تاریخِ انقضای فعلی** حساب میشود | تمدیدِ زودهنگام روزهای باقیمانده را پاک میکرد |
|
||||
| `subscription/status` اشتراکِ منقضی را `active: false` میدهد | فقط ستونِ status دیده میشد |
|
||||
| **یک کدِ تخفیف برای هر سفارش، و فقط پیش از رفتن به درگاه** | کدها جمع میشدند؛ و تخفیف بعد از گرفتنِ authority یعنی پرداختِ مبلغِ قبلی و ردِ verify با مبلغِ جدید |
|
||||
| **کدِ ۱۰۰٪ خرید را همانجا تمام میکند** (`gateway='discount'`) | سفارشِ صفرتومانی را هیچ درگاهی نمیپذیرد |
|
||||
| **پرداختِ تأییدنشده دوباره سنجیده میشود**: هر بار که اپ `subscription/status` یا جزئیاتِ سفارش را میخواند (حداکثر دقیقهای یک بار برای هر سفارش) و با `manage.py payment_reconcile` | اگر زرینپال موقعِ کالبک جواب نمیداد، پول کم شده بود و سفارش برای همیشه pending میماند. فقط به سمتِ paid، هرگز failed |
|
||||
| `/api/payment/webhook` حذف شد | یک «این سفارش پرداخت شد»ِ عمومی بود پشتِ یک رمزِ مشترک، که زرینپال اصلاً صدایش نمیزد |
|
||||
|
||||
## برنامهی معرفی (affiliate)
|
||||
|
||||
کاربرِ واردشده با شماره کدِ معرف میگیرد (`GET /api/affiliate/me`) و کاربرِ
|
||||
تازه کدِ دیگری را ثبت میکند (`POST /api/affiliate/track`). **پورسانت فقط روی
|
||||
اولین پرداختِ واقعیِ کاربرِ معرفیشده** حساب میشود
|
||||
(`affiliate.services.record_paid_order`، از داخلِ تأییدِ پرداخت):
|
||||
`commission_amount = مبلغِ پرداختی × commission_rate` و Conversion `paid` میشود.
|
||||
مهمان نه کد میگیرد نه معرفیشده حساب میشود (۴۰۳). تسویهی پورسانت با معرف هنوز
|
||||
دستی و از پنلِ ادمین است.
|
||||
|
||||
## دستیارِ هوشِ مصنوعی (RAG)
|
||||
|
||||
`POST /api/ai/ask` پاسخ را از محتوای واقعیِ کتاب میسازد: embedding سوال →
|
||||
@@ -79,6 +124,32 @@ PaymentRequest را میزند، `Order.gateway_authority` را ذخیره م
|
||||
کیفیتِ محتوایی که این لایه میخواند در
|
||||
[`content-authoring.md`](content-authoring.md) توضیح داده شده.
|
||||
|
||||
### شکلِ پاسخ (پرامپتِ `tutor-v3-stepwise-plain`، ۲۰۲۶-۰۹-۲۵)
|
||||
|
||||
| چه | چرا |
|
||||
|---|---|
|
||||
| **متنِ ساده، بدونِ LaTeX/markdown**؛ `rag/services/mathtext.py` هرچه از دستِ مدل در برود را به یونیکد برمیگرداند (`x²`، `√3/2`، `≤`) | اپ پاسخ را خام نشان میدهد. همهی جوابهای ذخیرهشدهی پایههای ۸ تا ۱۲ پر از `\frac` و `###` بودند |
|
||||
| **حلِ کامل، مرحلهبهمرحله، هر مرحله یک خط، وارسی پیش از «✅ جواب»، و یک تمرینِ مشابه** | تصمیمِ صاحبِ پروژه. وارسی به این خاطر است که جوابی مشتقِ x³−6x²+9x را 3x²−12x نوشته بود |
|
||||
| **لحنِ سهسطحی** (دبستان / متوسطهی اول / دبیرستان) | «پایهی سوم» در پرامپت بهتنهایی لحن را عوض نمیکرد |
|
||||
| **نمادِ اعداد تابعِ پایه**: دبستان رقمِ فارسی و «/» مثلِ کتاب برای ممیز، کسر با کلمه («۳ بر ۴»)؛ از هفتم به بعد ممیز با نقطه و «/» فقط برای کسر | در کتابهای ایران «۲/۵» یعنی دو و نیم، نه کسر |
|
||||
| **حافظهی گفتگو**: ۶ پیامِ آخر به مدل میرود و سوالِ قبلیِ کاربر به جستجو اضافه میشود | «نفهمیدم» برای مدل هیچ مرجعی نداشت |
|
||||
| **کش فقط برای پیامِ اولِ گفتگو**، و کلیدش نسخهی پرامپت را دارد | «نفهمیدم»ِ یک نفر جوابِ کششدهی گفتگوی دیگری را میگرفت |
|
||||
| **راهحلی که `rag_verify_solutions` ردش کرده (`disagrees`) به مدل نمیرسد**؛ فقط روش و صورتِ مسئله | حدودِ ۱۳٪ تکههای کتابهای دهم تا دوازدهم اینطورند و دستیار غلطِ کتاب را تکرار میکرد |
|
||||
|
||||
**انتخابِ مدل:** `OPENAI_CHAT_MODEL` برای همه، و `OPENAI_CHAT_MODEL_UPPER`
|
||||
(اختیاری) فقط برای دهم تا دوازدهم. مدلهای reasoning (o-series، gpt-5) بدونِ
|
||||
temperature و با `OPENAI_CHAT_REASONING_EFFORT` صدا زده میشوند. انتخاب با
|
||||
`python manage.py rag_model_benchmark` سنجیده میشود: مسئلههای تأییدشدهی کتاب،
|
||||
از مسیرِ واقعیِ retrieval + پرامپت، با داورِ ثابت و هزینهی واقعی از
|
||||
`/dashboard/billing/usage`. `LessonChunk` فقط در دیتابیسِ محلی است و GapGPT فقط
|
||||
از ایران جواب میدهد، پس نمونه با `--export-sample` محلی ساخته و روی سرور با
|
||||
`--sample-file` اجرا میشود. `--resume` مدلهای تمامشده را دوباره اجرا نمیکند.
|
||||
|
||||
**تمام شدنِ اعتبار دیده میشود.** هر گذارِ provider بینِ `ok` و
|
||||
`quota_exhausted` یک ردیف در `statuslog` است (`ai_tutor/provider_status.py`) و
|
||||
`/api/health` آن را در `integrations.aiProvider` و `warnings` نشان میدهد.
|
||||
پیشتر `openai: true` فقط یعنی «کلید تنظیم شده».
|
||||
|
||||
## ساختِ آزمون
|
||||
|
||||
اپ در سطحِ **نود** بازی میشود (جزیره روی نقشه)، ولی سوالها به **درس** وصلند —
|
||||
@@ -97,9 +168,26 @@ PaymentRequest را میزند، `Order.gateway_authority` را ذخیره م
|
||||
آزمونش تمام شده `completed` میبیند، و مسیرِ قدیمیِ «همهی درسها تکتک» هم هنوز
|
||||
کار میکند.
|
||||
|
||||
**جزیره با درسها هم تمام میشود، ولی فقط درسهای سوالدار شمرده میشوند**
|
||||
(۲۰۲۶-۰۹-۲۵). ۵۸۵ درس هنوز سوال ندارند و اپ برایشان دکمهی شروع ندارد؛ با شرطِ
|
||||
«همهی درسها» ۱۸۱ جزیره از ۲۶۹ هرگز تمام نمیشد. هر نود در `/api/home`
|
||||
`nextLessonId` دارد (اولین درسِ سوالدارِ تمامنشده) و **عضوِ اولِ `lessonIds`
|
||||
همیشه همان است** — فرانت با کلیک روی جزیره `lessonIds[0]` را باز میکند و پیشتر
|
||||
هر بار درسِ اول را میگرفت. جزیرهای که یک بار تمام شد (`NodeProgress` یا
|
||||
آزمون) تمام میماند، حتی اگر بعداً درسی سوالِ تازه بگیرد؛ سوالِ تازه کشِ نقشه را
|
||||
باطل میکند.
|
||||
|
||||
`progressPercent` در `/api/home` برای همان پایهای است که نقشهاش نشان داده
|
||||
میشود: درسهای تمامشده بهاضافهی همهی درسهای جزیرههای تمامشده، تقسیم بر
|
||||
درسهای آن پایه. (قبلاً مخرج همهی ۲٬۶۱۷ درسِ همهی پایهها بود.)
|
||||
میشود: درسهای سوالدارِ تمامشده بهاضافهی درسهای جزیرههای تمامشده، تقسیم
|
||||
بر درسهای سوالدارِ آن پایه. (قبلاً مخرج همهی ۲٬۶۱۷ درسِ همهی پایهها بود.)
|
||||
|
||||
## دستیار و سوالِ تمرینی
|
||||
|
||||
اگر اپ در `POST /api/ai/ask` فیلدِ `questionId` را بفرستد، دستیار صورتِ همان
|
||||
سوال، گزینهها، جوابِ درست و **آخرین جوابِ خودِ دانشآموز** را میبیند
|
||||
(`rag.services.generation.question_focus`) و اگر غلط بوده، اول میگوید اشتباه در
|
||||
کدام مرحله بوده. این همان «کمک در جایی که بچهها گیر میکنند» است. این
|
||||
جوابها کش نمیشوند. `userQuestion` حداکثر ۲۰۰۰ نویسه است.
|
||||
|
||||
## دستیار بدونِ درس
|
||||
|
||||
@@ -130,14 +218,37 @@ PaymentRequest را میزند، `Order.gateway_authority` را ذخیره م
|
||||
حالتِ پر کم شد، همه یکجا پر میشوند** (`HEART_FULL_REFILL_AFTER`، فیلدِ
|
||||
`full_refill_at`). `GET /api/user/hearts` هر دو زمان را میدهد: `nextRefillAt`
|
||||
و `fullRefillAt`. تا ۲۰۲۶-۰۹-۲۴ هر قلب ۲۴ ساعت بود؛ تایمرِ قدیمیِ بلندتر از ۵
|
||||
ساعت در اولین خواندن کوتاه میشود. بکاند جوابِ با صفر قلب را رد نمیکند؛
|
||||
توقف کار فرانت است.
|
||||
ساعت در اولین خواندن کوتاه میشود.
|
||||
|
||||
**با صفر قلب، بکاند فقط وقتی جواب را رد میکند که `HEARTS_ENFORCED=1` باشد**
|
||||
(۴۰۳ با `code: "no_hearts"` و `nextRefillAt`). پیشفرض خاموش است چون فرانتِ فعلی
|
||||
برای این پاسخ صفحه ندارد و دانشآموزِ بیقلب را به صفحهی خطای عمومی میبرد. تا
|
||||
روشن نشده، «قلبِ نامحدودِ» اشتراک فقط در ظاهرِ اپ اعمال میشود و با صدا زدنِ
|
||||
مستقیمِ API دور زده میشود.
|
||||
|
||||
**امتیاز فقط برای جوابِ ثبتشده است.** `lessons/{id}/complete` و
|
||||
`nodes/{id}/quiz/complete` عددِ `correctCount`ِ کلاینت را به تعدادِ سوالهای
|
||||
متمایزی که کاربر واقعاً درست جواب داده و در `UserAnswer` ثبت شده محدود میکنند؛
|
||||
پیشتر صدا زدنِ مستقیمِ این دو endpoint، بیجواب دادن، امتیاز و جدولِ رتبهبندی را
|
||||
پر میکرد. اپِ فعلی هر سوال را پیش از پایان ثبت میکند، پس برایش فرقی نکرد.
|
||||
|
||||
## دیتابیس
|
||||
|
||||
هنوز SQLite است (`OPTIONS.timeout=20` تا قفلهای همزمانیِ نوشتن را تحمل کند).
|
||||
مهاجرت به Postgres انجام نشده.
|
||||
|
||||
**`transaction_mode: IMMEDIATE`** (۲۰۲۶-۰۹-۲۵): هر `atomic()` قفلِ نوشتن را از
|
||||
اول میگیرد. SQLite از `SELECT … FOR UPDATE` پشتیبانی نمیکند و جنگو
|
||||
`select_for_update` را رویش بیصدا نادیده میگیرد؛ یعنی تا آن روز همهی
|
||||
«قفلِ ردیف»ها در پرداخت، سقفِ کدِ تخفیف، قلب و سهمیهی دستیار عملاً قفل نبودند.
|
||||
حالا کلِ تراکنشها سریالیاند (درشت ولی درست). فراخوانیهای شبکهای (AI،
|
||||
زرینپال) بیرون از `atomic()` هستند و باید بمانند. بعد از Postgres این گزینه
|
||||
برداشته میشود.
|
||||
|
||||
تستها روی فایلِ `.test-db.sqlite3` اجرا میشوند، نه دیتابیسِ حافظهای: آن یکی
|
||||
shared-cache است و قفلش جدولبهجدول و بیانتظار است، رفتاری که روی فایلِ واقعی
|
||||
پیش نمیآید.
|
||||
|
||||
**idِ هر پایه همان شمارهی پایه است** (سوم=۳، هشتم=۸، دوازدهم=۱۲) و در فیلدِ
|
||||
`Grade.number` هم هست. پایهی تازه id را از شمارهاش میگیرد، نه از شمارندهی
|
||||
دیتابیس؛ شماره اگر خالی بماند از عنوان («پایه چهارم») خوانده میشود. پایهی
|
||||
|
||||
Reference in New Issue
Block a user