52 KiB
عملیات
دیپلوی، پیکربندی، سلامت و بازیابی. برای شروعِ توسعهی محلی README.md را
ببینید.
دیپلوی
ssh -t administrator@78.38.35.249 \
'cd ~/mathapp-v2 && ./deploy.sh; sudo systemctl restart zareian-mathapp'
دو بار رمز میپرسد — یکبار برای ssh و یکبار برای sudo. منتظرِ هر پرامپت
بمانید؛ چسباندنِ چند خط با هم، متنِ صفشده را به پرامپتِ رمز میدهد و بعد از
سه تلاش قفل میشوید.
deploy.sh به ترتیب: fetch، برگرداندنِ درخت به origin/<branch>، نصبِ
وابستگیها، migrate، seed_content، collectstatic، ریاستارت، و در پایان
poll کردنِ /api/health.
دو رفتارش را بدانید:
- سرور را تعمیر میکند، جلویش نمیایستد. فایلِ دستیویرایششده قبلاً
git pull --ff-onlyرا میشکست و چون بقیهی مراحل رد میشدند، سرور بیصدا روی کدِ قدیمی میماند. حالا تغییراتِ محلی در.deploy-backups/بهصورتِ patch ذخیره و بعد باgit reset --hardدور ریخته میشوند، و کار ادامه مییابد. هر اجرای کامل، سرور را دقیقاً روی همان چیزی میگذارد که در Gitea است. - ریاستارت را اثبات میکند. زمانِ
ActiveEnterTimestampرا قبل و بعد مقایسه میکند و اگر عوض نشده باشد با کدِ غیرِصفر خارج میشود. قبلاً فقط آدرسِ عمومی را چک میکرد و پروسهی قدیمی همان200را میداد — سرور دو روز کدِ قدیمی سرو کرد در حالی که همهی فایلها روی دیسک تازه بودند.
فایل روی دیسک، فایلِ دیپلویشده نیست. قبل از باور کردنِ هر چیزی:
systemctl show zareian-mathapp -p ActiveEnterTimestamp
متغیرهای اختیاری: SKIP_PULL=1، TADRISINO_SERVICE، TADRISINO_HEALTH_URL،
PIP_INDEX_URL (وقتی pypi.org تایماوت میدهد)، DEPLOY_BUNDLE.
- سلامت از سوکتِ gunicorn (مسیرش را از یونیت میخواند) با هدرِ
Hostدامنهی خودمان سنجیده میشود، چون از داخلِ سرور آدرسِ عمومی از مسیرِ ArvanCloud برمیگردد و ۵۰۴/۰۰۰ میداد (۲۰۲۶-۱۰-۰۷). - اگر سرور به Gitea نمیرسد (از سرور ۵۰۴، از لپتاپ ۲۰۰): روی لپتاپ
git bundle create x.bundle <هشِ origin/main روی سرور>..main،scpبه~/mathapp-v2/.deploy-backups/، و روی سرورDEPLOY_BUNDLE=.deploy-backups/x.bundle ./deploy.sh. هشِorigin/mainکه چاپ میشود را با لپتاپ مقایسه کنید. بدونِ این متغیر جایگزینی نیست و نرسیدن به Gitea دیپلو را میایستاند.
بررسیِ سلامت
curl https://mathapi.tadrisino2.ir/api/health # عمومی: فقط {"status": "ok"|"error"}
curl -H "X-Internal-Key: <INTERNAL_SERVICE_KEY>" https://mathapi.tadrisino2.ir/api/health # جزئیات
کلید در .env سرور است (INTERNAL_SERVICE_KEY)؛ جایی ننویسید و در چت نفرستید. مستنداتِ API (/api/docs/، /api/redoc/، /api/schema/) فقط برای ادمینِ واردشده به
/admin/ باز است؛ برای فرانت: python manage.py spectacular --file schema.yaml --format openapi.
بدونِ احرازِ هویت و بدونِ SSH میگوید: دیتابیس سالم است یا نه، مهاجرتِ
اعمالنشده مانده یا نه، کش کار میکند یا نه، چند پایه/درس/سوال روی آن سرور
هست، کدام سرویسِ بیرونی تنظیم شده (فقط بله/خیر)، و چه هشدارهای امنیتیای
فعالاند. 200 سالم، 503 خرابیِ واقعی. هیچ سکرتی برنمیگرداند.
مهمترین بررسیاش «مهاجرتِ اعمالنشده» است: شایعترین حالتِ دیپلویِ خراب این
است که کد رفته ولی migrate اجرا نشده — اپ بالا میآید و بعد زیرِ بارِ واقعی
با خطای ستونِ ناموجود میشکند.
تأییدِ سریعِ اینکه API واقعاً جواب میدهد:
curl -i https://mathapi.tadrisino2.ir/api/schema/
curl -i https://mathapi.tadrisino2.ir/api/health
بکاپ و بازیابی
دیتابیسِ سرور Postgres است (cluster جدای zareian، پورت ۵۴۴۰، فقط localhost؛ راهنمای کامل: گزارش-۲۹/راهنمای-Postgres-و-Redis.html).
| لایه | چه چیزی | از دست رفتنِ داده در بدترین حالت | کجا |
|---|---|---|---|
| کپیِ زنده (اصلی) | هر تغییرِ دیتابیس لحظهای به سرورِ دیگر میرود + یک نسخهی کامل در روز؛ بازیابی تا هر لحظه از ۷ روزِ اخیر | چند ثانیهی آخر | administrator@78.38.35.247:~/zareian-pgbackup/ |
| پیش از migrate | ./deploy.sh (pg_dump -Fc؛ اگر نشد migrate اجرا نمیشود)؛ برای برگشتن از migrationِ خراب |
— | ~/mathapp-v2/.deploy-backups/db-before-deploy-*.pgdump (۲ تای آخر) |
| ایمیجِ کلِ ماشین | کارِ ادمینِ سرور، روزانه؛ ما محتوایش را نسنجیدهایم | — | دستِ ادمین |
بکاپِ شبانهی جداگانه (backup_db با cron) حذف شد: کپیِ زنده همان کار را بهتر میکند و ایمیجِ روزانه را ادمین دارد.
دفترِ خرید (PaymentEvent) روی هر دو سرور
هر قدمِ مسیرِ خرید یک ردیف در جدولِ payment_paymentevent مینویسد (payment/events.py): order_created،
discount_applied، gateway_redirect، gateway_request_failed، callback_received، callback_unknown_authority،
payment_result (همهی مسیرها: کالبک، بازسنجی، بازار، تمدیدِ بازار، کدِ ۱۰۰٪؛ با ignored برای تکراری/جعلی)،
bazaar_start، admin_cancel، admin_refund. توکنِ خریدِ بازار در آن نمیرود. ادمین: «Payment events» (فقطخواندنی).
چون جدول در همان Postgres است با WAL لحظهای به ۲۴۷ میرسد. روی ۲۴۷، zareian-pg-export-events.timer
(۰۱:۱۵، ۰۷:۱۵، ۱۳:۱۵، ۱۹:۱۵) با bin/export-payment-events.sh از روی همان بکاپ (نمونهی موقت روی سوکتِ داخلِ پوشه،
بیاتصال به ۲۴۹ یا Postgresِ دیگران) سه CSVِ تجمعی در ~/zareian-pgbackup/payment-log/ مینویسد:
payment-events-*، orders-*، transactions-*؛ ۱۴ تای آخرِ هر نوع میماند. شکستش در
journalctl -u zareian-pg-export-eventsِ ۲۴۷. زیرِ ۳ گیگ فضای آزاد اجرا نمیشود.
لاگِ برنامه (app.log): برای همیشه روی ۲۴۷
با LOG_FILE_ROTATION=external در .envِ ۲۴۹ برنامه لاگ را پاک نمیکند (WatchedFileHandler). هر شب ۰۰:۰۵
zareian-applog-archive.timer (۲۴۹) با ops/applogs/archive-app-log.sh فایل را به logs/archive/app-<UTC>.log.gz
میبرد و فشرده میکند؛ هر ساعت zareian-applog-pull.timer (۲۴۷) با bin/pull-app-logs.sh فایلهای تازه را به
~/zareian-pgbackup/app-logs/ میآورد (gzip -t پیش از نگهداشتن) و هرگز پاک نمیکند (تصمیمِ مالک ۲۰۲۶-۱۰-۱۳).
روی ۲۴۹ فقط ۳۰ روز میماند و فقط فایلی پاک میشود که ۲۴۷ گرفتنش را تأیید کرده (.shipped).
دسترسی: کلیدِ ~/.ssh/zareian_applogsِ ۲۴۷ در authorized_keysِ ۲۴۹ با
restrict,command="…/ops/applogs/serve-archive.sh" است: فقط list، get NAME، ack NAME؛ شِل و پورت ندارد
(سنجیده: id و cat /etc/passwd → bad request). بکاپِ authorized_keys پیش از افزودن کنارش است.
حجم (برآورد، نه سنجش): لاگِ JSON حدودِ ۱۵ برابر فشرده میشود (۷۰۰ک → ۴۰ک در اولین اجرا)؛ با ۶۰۰ کاربرِ فعال
حدودِ ۳۰ مگِ خام و ۲ تا ۳ مگِ فشرده در روز، یعنی حدودِ ۱ گیگ در سال روی ۲۴۷. اگر آرشیوِ شبانه بخوابد و
app.log از ۲۵۶ مگ بگذرد، /api/health هشدار میدهد. خواندن: zcat app-*.log.gz | grep user_id=26.
کپیِ زنده چطور کار میکند
Postgres هر تغییر را اول در یک دفترِ پیوسته (WAL) مینویسد. سرورِ ۲۴۷ این دفتر را با ابزارِ خودِ Postgres
(pg_receivewal) لحظهای میگیرد، نه کپیِ کلِ دیتابیس: فقط تغییرات میروند. روزی یک بار هم یک نسخهی کامل
(pg_basebackup) گرفته میشود؛ ۷ نسخهی آخر میمانند و WALِ قدیمیتر از قدیمیترینشان پاک میشود، پس حجم
محدود است. بازیابی = آخرین نسخهی کاملِ پیش از لحظهی دلخواه + بازپخشِ WAL تا آن لحظه.
۲۴۹ (اپ) ۲۴۷ (بکاپ)
Postgres 5440 (localhost) ◄── تونلِ SSH ─── zareian-pg-tunnel (127.0.0.1:15440)
اسلاتِ zareian_247 zareian-pg-wal → wal/ (لحظهای)
نقشِ zareian_replicator zareian-pg-basebackup.timer → base/ (هر روز ۰۴:۳۰، ۷ تا)
- ۲۴۹ را نمیخواباند: اپ منتظرِ ۲۴۷ نمیماند (تکثیرِ ناهمزمان). اگر ۲۴۷ قطع شود، اسلات تغییرات را نگه میدارد تا
وصل شود، ولی حداکثر ۱ گیگ (
max_slot_wal_keep_size)؛ بیش از آن اسلات «lost» میشود و دیسکِ ۲۴۹ امن میماند. نسخهی کامل با--checkpoint=spread --max-rate=20Mگرفته میشود. - امنیت: کلیدِ SSHِ ۲۴۷ در
authorized_keysِ ۲۴۹ باrestrict,port-forwarding,permitopen="127.0.0.1:5440"محدود است (نه شِل، نه پورتِ دیگر). رمزِ نقشِzareian_replicatorفقط در~/.pgpassِ ۲۴۷ است. این نقش همهی داده را میخواند (طبیعتِ بکاپ)، پس~/zareian-pgbackupبا مجوز ۷۰۰ است. ترافیک در شبکهی داخلی است (۲۴۹ =192.168.200.164، ۲۴۷ =192.168.200.167). - Postgresِ ۲۴۷ را به کار نمیگیریم: سرویسِ
postgresql@14-mainآنجا مالِ دیگران است. ما فقط ابزارهای Postgres 14 را اجرا میکنیم؛ آزمونِ بازیابی یک Postgresِ موقت روی سوکتِ داخلِ پوشهی خودمان بالا میآورد. - پایش:
/api/health(باX-Internal-Key) زیرِoffsiteBackupوضعیت و عقبماندگی را میدهد و اگر ۲۴۷ وصل نباشد، عقب باشد یا اسلات lost شده باشد هشدار میدهد (OFFSITE_WAL_SLOT=zareian_247در.envِ ۲۴۹). شکستِ نسخهی کاملِ روزانه از ۲۴۹ دیده نمیشود: روی ۲۴۷systemctl status zareian-pg-basebackupوjournalctl -u zareian-pg-basebackup. شکستش داده را از دست نمیدهد (WAL از آخرین نسخهی سالم نگه داشته میشود)، فقط بازیابی را کندتر میکند.
فایلها در ops/pgbackup/ ریپو. روی ۲۴۷: یونیتها در /etc/systemd/system/zareian-pg-*، اسکریپتها در
~/zareian-pgbackup/bin/. هر دو با دست کپی میشوند (deploy.sh فقط ۲۴۹ را میشناسد)؛ بعد از تغییرِ
یکی از آنها: کپی، sudo systemctl daemon-reload، و restart همان یونیت.
آزمونِ بازیابی (ماهی یک بار)
روی ۲۴۷: ~/zareian-pgbackup/bin/restore-check.sh (یا با یک لحظه: … '2026-10-10 16:30:00+03:30').
آخرین نسخهی کامل + همهی WAL را در پوشهی موقت بالا میآورد، شمارش و آخرین زمانِ کاربر/ورود/سفارش/تراکنش/
تغییرِ وضعیت را چاپ میکند و پاک میکند. همان پرسش را روی ۲۴۹ بزنید؛ باید یکی باشند (جز تغییرِ همان چند ثانیه).
اگر ۲۴۷ مدتی قطع بود
/api/health میگوید disconnected. با برگشتنِ ۲۴۷ سرویسها خودشان وصل میشوند و عقبماندگی را میگیرند. اگر
lost شد (بیش از ۱ گیگ تغییر در قطعی): روی ۲۴۷
systemctl stop zareian-pg-wal، روی ۲۴۹ اسلات را بیندازید
(SELECT pg_drop_replication_slot('zareian_247'))، روی ۲۴۷ بسازید
(pg_receivewal -h 127.0.0.1 -p 15440 -U zareian_replicator --create-slot --slot=zareian_247)،
systemctl start zareian-pg-wal و systemctl start zareian-pg-basebackup (نسخهی کاملِ تازه؛ WALِ پیش از آن
بریده است).
بازیابیِ واقعی
- دیتابیسِ ۲۴۹ خراب شد یا دادهای اشتباهی پاک شد: لحظهی درست را پیدا کنید، روی ۲۴۷ با
restore-check.shتا آن لحظه آزمایش کنید؛ وقتی درست بود، باKEEP=1اجرا کنید تا Postgresِ موقت بماند، از آن باpg_dump -Fc -h <پوشه> -U postgres zareian_mathappخروجی بگیرید و روی ۲۴۹ باpg_restore --clean --no-owner -d zareian_mathappبرگردد (اپ را پیش از آن stop کنید و از وضعیتِ فعلی هم یکpg_dumpبگیرید). این مسیر روی سرور آزموده نشده؛ با کمک بروید. - کلِ ۲۴۹ رفت: همان بازیابی تا انتهای WAL روی ماشینِ تازه، و سفارشهای چند ثانیهی آخر را با پنلِ زرینپال/بازار تطبیق دهید.
متغیرهای محیطی
.env خودکار خوانده میشود (python-dotenv) و مقادیرش با پیشفرضهای کد
ادغام میشوند، نه جایگزینشان. پس یک .env ناقص روی سرور دیگر باعثِ بالا
نیامدن نمیشود. .env.example توضیحِ کاملِ هر کدام را دارد.
| متغیر | اگر ست نشود چه میشود |
|---|---|
DJANGO_DEBUG |
خاموش (0) — که حالتِ درستِ پروداکشن است |
DJANGO_SECRET_KEY |
یک کلیدِ تصادفیِ قوی ساخته و در .django_secret_key (مجوز 0600) ذخیره میشود |
PUBLIC_BASE_URL · FRONTEND_URL |
دامنهی فعلی (mathapi.tadrisino2.ir / math.tadrisino2.ir). بقیهی تنظیمهای دامنه از همین دو ساخته میشوند؛ بخشِ «عوض کردنِ دامنه» |
DJANGO_ALLOWED_HOSTS · CSRF_TRUSTED_ORIGINS · CORS_ALLOWED_ORIGINS |
از دو آدرسِ بالا ساخته میشوند؛ مقدارِ env فقط دامنهی اضافه است |
CACHE_URL |
سرویس بالا میآید ولی نرخ بینِ workerها مشترک نیست؛ /api/health هشدار میدهد. روی سرور به Redisِ اختصاصیِ خودمان (zareian-redis، پورت ۶۳۹۰) اشاره میکند، نه Redisِ مشترکِ ماشین. با آن، قطعیِ Redis API را ۵۰۰ نمیکند ولی تا برگشتنش سقفهای مبتنی بر کش اعمال نمیشوند و health ۵۰۳ میدهد (config/cache.py) |
KAVENEGAR_API_KEY |
POST /api/auth/otp/request خطای صریحِ 503 میدهد، نه موفقیتِ ساختگی |
ZARINPAL_MERCHANT_ID |
مسیرِ پرداخت fail-closed میماند (503) |
gunicorn.conf.py (در ریپو، نه env) |
gunicorn خودش از پوشهی کاری میخواندش: ۴ workerِ gthread با ۳۲ نخ و timeoutِ ۱۲۰. تعدادِ workerها همینجاست؛ یونیتِ systemd دیگر --workers ندارد (اگر داشته باشد بر این فایل مقدم است) |
DB_POOL_MAX_SIZE · DB_POOL_MIN_SIZE · DB_POOL_TIMEOUT |
۸ · ۲ · ۱۰ ثانیه: هر worker حداکثر ۸ اتصالِ Postgres دارد و نخها آن را فقط تا پایانِ درخواست قرض میگیرند. اگر تا ۱۰ ثانیه اتصالی آزاد نشد، درخواست ۵۰۳ با Retry-After میگیرد (نه ۵۰۰). workers × DB_POOL_MAX_SIZE + 6 باید زیرِ max_connections (روی سرور ۶۰ − ۳) بماند؛ /api/health → dbPool آن را میسنجد و هشدار میدهد. DB_CONN_MAX_AGE دیگر پذیرفته نمیشود (برنامه بالا نمیآید) |
AI_MAX_CONCURRENT_PER_WORKER |
۱۲: حداکثر جوابِ همزمانِ دستیار در هر worker (۴ × ۱۲ = ۴۸). هر جواب یک نخ را تا ۴۰ ثانیه نگه میدارد؛ سقف نمیگذارد رگبارِ سوال همهی ۳۲ نخ را بگیرد و نقشه و آزمون بخوابند. بیشتر از آن ۵۰۳ «دستیار شلوغه» با Retry-After میگیرد، پیش از رزروِ سهمیه. /api/health → aiConcurrency |
DB_STATEMENT_TIMEOUT_MS |
۳۰٬۰۰۰: هر کوئریِ بیش از ۳۰ ثانیه لغو میشود و تراکنشِ بازِ بیکار بعد از ۶۰ ثانیه بسته میشود. برای فرمانِ مدیریتیِ طولانیِ دستی: DB_STATEMENT_TIMEOUT_MS=0 python manage.py … |
ZARINPAL_REDIRECT_URL |
بعد از پرداخت کاربر به FRONTEND_URL/?orderId=…&status=paid|failed برمیگردد. ZARINPAL_REDIRECT_URL=- یعنی همان پاسخِ JSONِ قدیمی |
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 را میدهد |
TEST_LOGIN_PHONES |
خالی، و روی سرور باید خالی بماند (بخشِ «ورود بیپیامک») |
HEARTS_ENFORCED |
روشن (از ۲۰۲۶-۱۰-۰۲): با صفر قلب نه جواب پذیرفته میشود نه پایانِ درس/جزیرهی بیجواب. 0 خاموشش میکند |
INTERNAL_SERVICE_KEY |
endpointهای داخلیِ ingestِ RAG و ساختِ سفارشِ سرور-به-سرور بستهاند (در DEBUG هم؛ پیشفرض ندارد) |
اتصال به بازار — روشِ جدید (ترجیحی): در پیشخانِ بازار روی برنامه ← «API پیشخان
بازار» ← «دریافت توکن جدید» (از حسابِ ناشر، نه کارپرداز). همان را در .env بهعنوانِ
BAZAAR_PISHKHAN_API_SECRET بگذارید؛ در هدرِ CAFEBAZAAR-PISHKHAN-API-SECRET فرستاده
میشود و redirect و انقضا ندارد. اگر ست باشد روشِ قدیمیِ زیر نادیده گرفته میشود.
اتصال به بازار — روشِ قدیمی (OAuth، یکبار): Redirect URIِ ثبتشده در پنلِ بازار
<PUBLIC_BASE_URL>/api/payment/bazaar/verify است و BAZAAR_REDIRECT_URI خودش از همان ساخته
میشود؛ این دو باید عیناً یکی باشند. با عوض شدنِ دامنه، اول آدرسِ پنلِ بازار عوض شود، بعد
لینکِ تازه ساخته شود (لینکِ قبلی آدرسِ قدیمی را در خودش دارد). در ادمین، «اتصال به API بازار» → actionِ «ساختِ لینکِ
اتصال» (یا python manage.py bazaar_authorize_link). صاحبِ حسابِ بازار آن لینک را
ظرفِ سه روز (BAZAAR_AUTHORIZE_WINDOW_HOURS، پیشفرض ۷۲) باز میکند و «اجازه» میزند؛ بازار با ?code= به همان آدرس برمیگردد
و GETش refresh token را میگیرد و در دیتابیس نگه میدارد؛ پنجره با همین اتصالِ موفق
بسته میشود. POSTِ همان آدرس تأییدِ
خریدِ اپِ اندروید است. آدرسها با سندِ بازار مقایسه شدهاند (۲۰۲۶-۰۹-۲۶)؛ هنوز هیچ خریدِ واقعیای
از آنها رد نشده. سند سایتی JS است ولی متنش از
developers.cafebazaar.ir/wp-json/wp/v2/document?search=... خوانا است.
پرداختهای تأییدنشده: اپ هر بار که وضعیتِ اشتراک را میخواند، سفارشهای
pendingِ همان کاربر را دوباره با زرینپال میسنجد. برای دیدنِ همه یکجا:
python manage.py payment_reconcile --days 3.
DJANGO_SECRET_KEYتوکنهای ورود را امضا میکند. بهترین حالت این است که اصلاً ست نشود تا خودِ تنظیمات یکی بسازد — آنوقت کلیدی وجود ندارد که لو برود. هرگز نگذارید سرور روی fallbackِdjango-insecure-…کار کند: آن رشته در همین ریپو کامیت شده، پس هر کسی که ریپو را بخواند میتواند بهجای هر کاربری توکن جعل کند.manage.py check --deployبا کدِsecurity.W009علامتش میزند.
DJANGO_DEBUGهرگز روی سرور1نشود.DEV_MASTER_OTP(کد0000) را روشن میکند که هر شمارهای را بهعنوانِ ورودِ معتبر میپذیرد، و صفحههای خطا کلِ تنظیمات را نشان میدهند. جایش فقط.envاست، نه یونیتِ systemd.
عوض کردنِ دامنه
دامنههای فعلی: API روی mathapi.tadrisino2.ir و فرانت روی math.tadrisino2.ir.
دامنه در کد فقط یکجا آمده: پیشفرضِ PUBLIC_BASE_URL و FRONTEND_URL در
config/settings.py. ALLOWED_HOSTS، CSRF_TRUSTED_ORIGINS، CORS_ALLOWED_ORIGINS،
callbackِ زرینپال، برگشت از پرداخت، BAZAAR_REDIRECT_URI و آدرسِ سلامت در
deploy.sh همه از همین دو ساخته میشوند (config/tests_domains.py). فرانت هم دامنه را در کد
ندارد (README همان ریپو).
ترتیبِ کار، از بیرون به داخل:
- DNS، آروان و گواهی برای دامنههای تازه، رو به همین سرور، و
server_nameِ nginx — کارِ ادمینِ سرور، نه ما. - بکاند: در
.envسرورPUBLIC_BASE_URLوFRONTEND_URLرا عوض کنید. تا DNS کامل جابهجا شود، دامنههای قبلی را هم نگه دارید، مثلاًDJANGO_ALLOWED_HOSTS=<دامنهی قبلیِ API>وCORS_ALLOWED_ORIGINS=https://<دامنهی قبلیِ فرانت>. بعد ریاستارت. - زرینپال: دامنهی تازهی API را در پنلِ ترمینال ثبت کنید. خطای
-14(«دامنهی callback با دامنهی ثبتشده نمیخواند») دقیقاً نبودنِ همین است. بازار (اگر استفاده شود): Redirect URI در پنل باید عیناًPUBLIC_BASE_URL/api/payment/bazaar/verifyشود. - فرانت: روی سرورِ فرانت در
dist/config.jsبنویسیدapiBaseUrl: "https://<دامنهی تازهی API>/api"(یا.env.productionرا عوض و build کنید)، و کشِ آروان را برایconfig.jsوindex.htmlپاک کنید. - nginx و CSP: در
ops/nginx/zareian-front-headers.confدامنهی تازهی API را درconnect-srcبگذارید، روی سرور در/etc/nginx/snippets/نصب کنید (بخشِ «nginx»)؛ وگرنه مرورگر هر درخواستِ API را میبندد. - وارسی:
curl https://<دامنهی تازهی API>/api/health(درclientIpباید IPِ خودتان باشد) و یک بار ورود و باز کردنِ یک درس در اپ.
مرحلهی ۳ را فقط یک پرداختِ واقعی ثابت میکند؛ تا آن انجام نشده، پرداخت را «کار میکند» ننویسید.
لغو و بازپرداختِ سفارش (از پنلِ ادمین)
همه از /admin/payment/order/ انجام میشود و SSH لازم نیست:
- سفارش را با شمارهاش یا شناسهی کاربر (
user_id) پیدا کنید. شمارهی فاکتورِTD-000123یعنی سفارشِ ۱۲۳. - تیکش را بزنید و از فهرستِ «Action» یکی را انتخاب کنید:
- «لغوِ سفارشِ در انتظار»: برای سفارشی که هنوز پرداخت نشده. پولی جابهجا نشده است. اگر کاربر همان لحظه در درگاه پرداخت کرده باشد، تأییدِ زرینپال سفارش را دوباره «پرداختشده» میکند و او چیزش را میگیرد.
- «بازپرداخت»: برای سفارشِ پرداختشده. اول یک صفحهی تأیید میآید. بعد از تأیید، سفارش «بازپرداختشده» میشود و روزهای اشتراکِ همان سفارش از پایانِ اشتراک کم میشود (اگر چیزی نماند، اشتراک «لغوشده» میشود). پورسانتِ معرف هم برمیگردد. پیامِ بالای صفحه میگوید دقیقاً چه چیزی پس گرفته شد.
- خودِ پول را این دکمه برنمیگرداند. پول را در پنلِ زرینپال برگردانید. خریدِ کافهبازار هم از سمتِ خودِ بازار برگردانده میشود.
وضعیتِ سفارش در فرمِ ادمین فقطخواندنی است. اگر وضعیت را با دست از «پرداختشده» به «ناموفق» عوض
میکردید، اشتراکی که سفارش داده بود سرِ جایش میماند. کاربر بازپرداخت را در صفحهی
«پرداختها و فاکتورها» میبیند. کد در payment/services.py (refund_order، cancel_pending_order)
است و تستهایش در payment/tests_refund.py.
فروش در کافهبازار
اپِ اندروید (پوستهی WebViewِ همین سایت، android/ در ریپوی فرانت) با پرداختِ درونبرنامهایِ بازار میفروشد. در
برگهی پرداخت دو کاشی همیشه کنارِ هماند و کاربر هر کدام را بخواهد میزند: «کارتِ بانکی» (زرینپال، پیشفرض) و
«کافهبازار». در مرورگر، یا وقتی پلن SKU ندارد، کاشیِ بازار غیرفعال است و دلیلش را میگوید. فقط اشتراک فروخته میشود
(بستهی سوال از ریشه حذف شد، subscription.0017/0018). داخلِ اپ
زیرِ «کافهبازار» قیمتی میآید که خودِ بازار برای آن SKU دارد (Poolakey روی گوشی؛ API سرور قیمت نمیدهد).
تفاوتِ روشها در یک جاست: payment/providers.py (هر روش یک کلاس، provider_for/provider_for_method انتخابش
میکنند). روشِ تازه = یک زیرکلاس و یک خط در PROVIDERS.
| چه | کجا | الان |
|---|---|---|
| محصول در پیشخانِ بازار | محصولِ «اشتراکی» (بازار خودکار تمدید میکند) | یکماهه 10342، سهماهه 10343، ششماهه 10344 (تأییدِ سینیور) |
| SKU روی پلن | /admin/subscription/plan/ ← «Bazaar sku» (سرور SKU را فقط از اینجا میگیرد) |
سه اشتراکِ بالا (subscription.0014، 0015) |
| قیمت | بازار قیمتِ پیشخان را میگیرد؛ ادمین باید با آن یکی باشد | ۵۹۸/۸۹۸/۱٬۳۹۸ هزار تومان در هر دو (subscription.0016، به گفتهی سینیور)؛ داخلِ اپ قیمتِ بازار روی کاشی دیده میشود |
| کلیدِ «تخفیف پویا» | BAZAAR_DYNAMIC_PRICE_KEY در .env سرور |
با آن کدِ تخفیف روی خریدِ بازار هم کار میکند |
| کلیدِ RSAِ عمومی | android/app/build.gradle.kts |
گذاشته شد |
مسیر:
POST /api/subscription/purchaseباpaymentMethod: "bazaar"→ سفارش وbazaarSku.- (اختیاری)
POST /api/discount/redeemروی همان سفارش. فقط با کلیدِ «تخفیف پویا»، وگرنه ۴۰۰. POST /api/payment/bazaar/orders/{id}/start→ SKU، و اگر تخفیف خوردهdynamicPriceToken(JWTِ HS256 با مبلغ به ریال). بیتخفیف توکن نمیرود چون بازارِ قدیمیتر از ۱۳.۳ با آن خطا میدهد. از این لحظه کدِ تخفیف نمینشیند.- اپ با Poolakey اشتراک میخرد (
subscribeProduct،payload= شمارهی سفارش). POST /api/payment/bazaar/verifyتوکن را از خودِ بازار میسنجد و اشتراک را فعال میکند. اشتراک consume نمیشود.
تمدیدِ خودکار: بازار به ما خبر نمیدهد. وقتی دورهی پرداختشدهی کاربر (Order.bazaar_valid_until) کمتر از یک روز
مانده یا گذشته، با باز شدنِ وضعیتِ اشتراک (GET /api/subscription/status) همان توکن از بازار پرسیده میشود (هر توکن
ساعتی یک بار)، و همینطور وقتی کاربرِ بیاشتراکِ فعال خانه یا فصلِ پولی را باز میکند (has_subscription_access). اگر validUntil جلو رفته، برای هر دوره یک سفارشِ «پرداختشده» ساخته میشود و روزها اضافه میشوند
(payment.services.sync_bazaar_renewals). مبلغِ آن سفارش قیمتِ فعلیِ ادمین است، چون بازار مبلغِ تمدید را نمیگوید.
اگر بازار در دسترس نباشد، کاربرِ تمدیدشده تا تلاشِ ساعتِ بعد پشتِ دیوار میماند. لغوِ تمدید از خودِ بازار است.
تستها: payment/tests_bazaar_subscription.py، payment/tests_bazaar_oauth.py.
هیچ خریدِ واقعیای از بازار انجام نشده؛ نه پذیرفتنِ JWT از سوی بازار و نه تمدیدِ واقعی.
ورود بیپیامک (آزمون)
روی سرور هیچ شمارهای با کدِ ثابت نیست و نباید باشد: کدِ ثابت هر کس آن را در ارائه یا اسکرینشات
دیده باشد را وارد میکند. TEST_LOGIN_PHONES در .env سرور خالی است و /api/health اگر پر شود هشدار
میدهد. حسابهای دمو (کاربرهای ۱۱ تا ۱۵ و ۲۵) و همهی دادهشان با delete_users پاک شدند (بکاپِ پیش از آن:
.deploy-backups/db-before-demo-removal-20261013-190632.pgdump). حسابِ ۱۰ به شمارهی مالکِ پروژه وصل است و ماند.
راهِ آزمون بیپیامک: یک ابرکاربر در /admin/accounts/otpcode/add/ برای شمارهی خودش یک کدِ تصادفی با انقضای
چند دقیقه میسازد (یکبارمصرف، با همان سقفِ ۵ حدس). کارمندِ عادی این صفحه را ندارد
(StaffCannotMintLoginsTests). بعد از آزمون POST /api/auth/logout.
روی لپتاپ (DEBUG=1) کدِ devOtp در پاسخ میآید و TEST_LOGIN_PHONES=09120000001:834512 هم کار میکند؛
هیچکدام برای سرور نیست.
سرور
mathapi.tadrisino2.ir روی یک ماشینِ مشترکِ شرکت اجرا میشود (administrator@78.38.35.249)، نه سروری که مالِ این
پروژه باشد. روی همان ماشین Gitea، ایمیل، DNS، چند دیتابیس و اپهای تیمهای دیگر در حالِ کارند، و حسابِ ما sudoِ
کامل دارد؛ تنها مانع میانِ یک فرمانِ بیدقت و دادهی کلِ شرکت، خودِ اجراکننده است. سرورِ بکاپ (78.38.35.247) هم
مشترک است.
مالِ ما روی ۲۴۹: پوشههای ~/mathapp-v2 (بکاند) و ~/mathapp_frontend_new (فرانت)، سرویسِ gunicorn-mathapp با
نامِ دومِ zareian-mathapp (همیشه همین نام را بنویسید)، zareian-redis، clusterِ Postgresِ 14/zareian، تایمرِ
zareian-applog-archive، و دو سایت و دو snippetِ nginx (بخشِ بعد). روی ۲۴۷ فقط ~/zareian-pgbackup/ و یونیتهای
zareian-*.
sudo systemctl restart zareian-mathapp # مالِ ما
sudo systemctl restart gunicorn # مالِ ما نیست — اپِ تیمِ دیگر
قواعدی که قابلِ مذاکره نیستند (نسخهی کامل، با فهرستِ آنچه مالِ ما نیست و رویهی دیپلو، در CLAUDE.md «Working on
the servers»):
- اول فقط نگاه کنید، بعد تغییر بدهید؛ هیچ فرمانِ مخربی بیرون از پوشههای ما، و مسیرها را کامل بنویسید.
git cleanهرگز، و پروژه هرگز پاک و «از نو clone» نشود..env،.django_secret_key، لاگها و بکاپها در گیت نیستند و همانجا زندگی میکنند؛ راهِ برگشتgit reset --hard origin/mainاست.- فقط
zareian-mathappریاستارت شود؛ nginx مشترک است:nginx -tو بعدreload، هرگزrestart. - پیش از ویرایشِ هر چیز زیرِ
/etcیا.envبکاپِ تاریخدار در~/mathapp-v2/.deploy-backups/. .envفقط از راهِ Django خوانده شود؛cat .envنه، و مقادیرش در هیچ چتی paste نشود.df -h /پیش از هر کارِ پرنوشت. پر شدنِ دیسک ایمیل و Gitea را هم پایین میکشد.- از منابعِ تیمهای دیگر استفاده نشود (Redis، Postgres، صف، سایتِ nginx)؛ نیاز به آنها درخواست از ادمینِ سرور است.
nginx (دو سایتِ ما)
نسخهی اصلی در ریپو است: ops/nginx/. روی ۲۴۹: sites-enabled/mathapp (API، فایلِ معمولی)،
sites-available/mathapp-front (فرانت)، و دو snippet در /etc/nginx/snippets/:
| فایل | چه میکند |
|---|---|
zareian-arvan-only.conf |
فقط IPهای ArvanCloud و خودِ ماشین؛ درخواستِ مستقیم به IPِ سرور ۴۰۳. همان فهرستِ TRUSTED_PROXY_NETWORKS |
zareian-front-headers.conf |
CSP، X-Frame-Options، nosniff، Referrer-Policy، Permissions-Policy، HSTS برای فرانت |
mathapp |
سقفِ بدنه ۲۰ مگابایت، HSTS |
تغییر: فایلِ ریپو را ویرایش و کامیت کنید؛ روی سرور فایلِ فعلی را در .deploy-backups/ کپی کنید، sudo install -m 644،
sudo nginx -t، و فقط اگر موفق بود sudo systemctl reload nginx (نه restart: سایتهای دیگران روی همین nginxاند).
بعد از بیرون: هدرها با curl -sI، و درخواستِ مستقیم به IP باید ۴۰۳ بدهد.
وقتی دیگران به اینها بخورند:
- آروان رنجِ IPِ تازه گرفته: کاربرانِ پشتِ آن edge ۴۰۳ میگیرند و peerشان در
/var/log/nginx/access.logاست. فهرست را از https://www.arvancloud.ir/fa/ips.txt در snippet و درconfig/settings.pyبا هم تازه کنید. - اسکریپتِ درونخطیِ
index.htmlِ فرانت عوض شده: CSP آن را میبندد (کنسول: «Refused to execute inline script»). هشِ تازه را در snippet بگذارید (روش در خودِ فایل). - سنجیده (۲۰۲۶-۱۰-۱۳): اپ در Chrome، بیورود و با ورود، همهی تبها و آزمونِ یک جزیره، بی هیچ خطای CSP. نسنجیده: اپِ اندروید
(WebView همان CSP را میگیرد) و صفحهی
pay.htmlتا درگاه.
سرورِ اختصاصیِ جدید (78.38.35.216): آماده، هنوز سوئیچ نشده
اپ به یک ماشینِ مجازیِ اختصاصی منتقل میشود. کاربران هنوز از ۲۴۹ سرو میشوند؛ ArvanCloud هنوز به ۲۴۹ اشاره میکند.
| ماشین | VMware، Ubuntu 24.04، ۸ هسته، ۱۲ گیگ رم، دیسکِ ۹۷ گیگ؛ hostname mathapp، IPِ داخلی 192.168.200.163 (۲۴۹ = .164)، IPِ خروجی 78.38.35.228 |
| ورود | دو حساب با sudo: zareian (صاحبِ محصول) و mathapp-admin (مشترکِ سینیور و صاحبِ شرکت؛ رمزش در فایلِ رمز زیرِ AdminUser). temp حذف شد (۲۰۲۶-۱۰-۱۷؛ authorized_keysش در /home/zareian/backups/). ورودِ root بسته، AllowUsers zareian mathapp-admin، forwarding بسته (10-tadrisino.conf) |
| فایلها | نسخهی اصلیِ همهی اسکریپتها و تنظیمهای این سرور در ops/server216/ (بیکلید). تغییر: فایلِ ریپو، بعد کپی روی سرور |
| ضدِ ربات | ۱) tadrisino-blocklist.timer: هر روز FireHOL level1 (Spamhaus، DShield، …) در ipsetِ tadrisino-block، همهی پورتها؛ شبکهی داخلی، 78.38.35.0/24 و رنجهای آروان هرگز در آن نیستند. ۲) ufw limit 22: بیش از ۶ اتصال در ۳۰ ثانیه رد. ۳) fail2ban پلهای: ۶ اشتباه در ۱۰ دقیقه → ۱۰ دقیقه، هر تکرار دو برابر تا یک هفته؛ recidive: ۵ بار در روز → یک هفته همهی پورتها؛ 192.168.200.0/24 هرگز. آزاد کردن: sudo fail2ban-client set sshd unbanip <IP> |
| بکاپ | رمزنگاری روی همین سرور، ۲۴۷ فقط فایلِ رمزشده میبیند (۲۴۷ مشترک است و هر کس administrator را دارد root است). pgBackRest با repo1-cipher-type=aes-256-cbc: آرشیوِ پیوستهی WAL (archive_timeout=60، یعنی حداکثر یک دقیقه تغییرِ آرشیونشده)، tadrisino-pgbackrest-full یکشنبهها، -diff هر روز، دو نسخهی کامل نگه داشته میشود. CSVهای پرداخت هر ۶ ساعت با age رمز میشوند (tadrisino-export-payment-events). tadrisino-offsite-sync هر دقیقه /var/lib/pgbackrest و payment-log را به ~/zareian-pgbackrest-216/ روی ۲۴۷ میفرستد؛ کلیدِ postgres در authorized_keysِ ۲۴۷ با from=192.168.200.163,restrict,command="rrsync -wo …" فقط میتواند در همان پوشه بنویسد (نه بخواند، نه شِل). /api/health → offsiteBackup از pg_stat_archiver و OFFSITE_SYNC_STATUS_FILE |
| کلیدهای بازیابی | /etc/tadrisino-backup/RECOVERY-KEYS (root، 600): کلیدِ pgBackRest و کلیدِ age. بیآنها بکاپِ ۲۴۷ خواندنی نیست؛ یک نسخه بیرون از سرورها نزدِ صاحبِ محصول لازم است: ssh -t zareian@78.38.35.216 sudo cat /etc/tadrisino-backup/RECOVERY-KEYS در ترمینالِ خودش (نه در چت) |
| آزمونِ بازیابی | sudo -u postgres tadrisino-restore-check (ماهی یک بار): بازیابی در پوشهی موقت و مقایسهی شمارشها با دیتابیسِ زنده. بازیابیِ واقعی از ۲۴۷ (اگر ۲۱۶ از دست رفت): پوشهی repo/ را از ۲۴۷ کپی، pgbackrest.conf با کلید از RECOVERY-KEYS، pgbackrest --stanza=zareian restore |
| اپ | کاربرِ بیsudoِ mathapp؛ /home/mathapp/mathapp-v2 و /home/mathapp/mathapp_frontend_new |
| سرویسها | zareian-mathapp (= gunicorn-mathapp، با RuntimeDirectory)، zareian-redis (6390)، postgresql@16-zareian (5440)، nginx، zareian-applog-archive.timer |
| رمزها | رمزِ Postgres و Redis روی این سرور تازه ساخته شده (.pg_password، .redis_password کنارِ .env)، نه کپیِ ۲۴۹ |
| فایروال | ufw فقط 22/80/443. فایروالِ شبکهی شرکت 443ِ ورودی را هنوز میبندد (80 باز است) |
| بستهها | apt از mirror.arvancloud.ir، pip از mirror-pypi.runflare.com (~mathapp/.config/pip/pip.conf)، npm از mirror-npm.runflare.com؛ files.pythonhosted.org و registry.npmjs.org از این سرور بستهاند |
سنجیده: /api/health با همهی جزئیات ok، آزمونِ بازیابی از بکاپِ رمزشده با همان شمارشها، فایلهای ۲۴۷ رمزشده (Salted__،
age-encryption.org/v1)، کلیدِ ۲۱۶ روی ۲۴۷ نه میخواند نه شِل میگیرد، کپیِ ۲۴۷ با rsync --checksum عینِ مخزنِ محلی، Qdrant از این سرور وصل، فرانت ۲۰۰، ریبوت و
بالا آمدنِ خودکارِ همهی سرویسها، درخواستِ مستقیم به IP ۴۰۳ (nginx IPِ واقعی را میبیند)، بازیابیِ کامل از نسخهی رمزشدهی
خودِ ۲۴۷ (دیتابیس و CSV؛ عقبماندگیِ طراحیشده حداکثر ~۲ دقیقه). سفرِ کاملِ کاربر از nginx+TLS روی خودِ سرور (۲۹ مورد): فرانت و
هدرهای امنیتی، پیامکِ واقعیِ کاوهنگار و ورود، نقشه، آزمونِ جزیره و پایانش، پیِ زنده، سفارش و redirect به StartPayِ زرینپال (بیپرداخت)،
خروج و باطل شدنِ توکن. کاوهنگار و GapGPT از IPِ خروجیِ این سرور پذیرفتهاند. نسنجیده: هیچ درخواستی از مسیرِ ArvanCloud
(۴۴۳ بسته است)، پرداختِ کامل، خریدِ بازار.
چه چیزی منتظرِ دیگران است
درخواست از سینیور فرستاده شده (۲۰۲۶-۱۰-۱۷): ۱) 443ِ ورودی روی فایروالِ شرکت؛ ۲) (انجام شد: temp حذف و mathapp-admin ساخته شد)؛ ۳) عوض کردنِ originِ
math.tadrisino2.ir و mathapi.tadrisino2.ir در پنلِ ArvanCloud به 78.38.35.216 (ما به پنل دسترسی نداریم؛ همزمان با قدمِ ۴ پایین)؛
۴) private کردنِ مخزن در Gitea؛ ۵) ۱۲ هسته و رمِ بیشتر (اختیاری؛ سرور ریبوت را تحمل میکند، تست شده). زرینپال و بازار به
دامنه بستهاند نه IP، پس در پنلِ آنها چیزی عوض نمیشود.
سوئیچ، قدمبهقدم (وقتی سینیور گفت آماده است)
- پیششرط: از لپتاپ
curl -sk -o /dev/null -w '%{http_code}' https://78.38.35.216/باید ۴۰۳ بدهد (نه ۰۰۰: یعنی 443 باز است و nginx فقط آروان را راه میدهد). کامیتها push شده باشند وdeploy.shروی هر دو سرور اجرا شده باشد (کدِ دو طرف یکی). - ۲۴۹ را بخوابان:
sudo systemctl stop zareian-mathapp(از این لحظه کاربران خطا میبینند تا قدمِ ۵؛ کلِ کار ~۱۵ دقیقه). - دیتابیسِ نهایی:
sudo -u postgres pg_dump -p 5440 -Fc zareian_mathappروی ۲۴۹، با pipe از لپتاپ به/home/zareian/stage/روی ۲۱۶ (مسیرِ داخلیِ ۲۴۹→۲۱۶ کلید ندارد). - روی ۲۱۶:
systemctl stop zareian-mathapp؛dropdb -p 5440 zareian_mathapp && createdb -p 5440 -O zareian_app zareian_mathapp(باsudo -u postgres)؛pg_restore -p 5440 -d zareian_mathapp --no-owner --role=zareian_app --exit-on-error؛ شمارشِaccounts_user،content_question،payment_order،payment_paymentevent،content_useranswerبا همان پرسش روی ۲۴۹ یکی باشد؛systemctl start zareian-mathapp؛ health باX-Internal-Keyبیهشدار. دادههای تستِ ۲۰۲۶-۱۰-۱۷ روی ۲۱۶ با همین قدم پاک میشوند. - سینیور origin را در آروان عوض میکند. بعد از بیرون:
https://mathapi.tadrisino2.ir/api/healthok؛ در/var/log/nginx/access.logِ ۲۱۶ IPهای آروان دیده شوند؛ ورود در Chrome (روشِ OTPِ ادمین، بخشِ «ورود بیپیامک») و یک آزمون؛ کاربرِ اندروید (WebView وpay.html) هم یک بار. - بکاپِ تازه:
sudo systemctl start tadrisino-pgbackrest-fullو بعدtadrisino-offsite-sync؛sudo -u postgres tadrisino-restore-check. - ۲۴۹ را کنار بگذار (پاک نکن):
sudo systemctl disable zareian-mathappروی ۲۴۹؛ اسلات را بردار تا WAL روی دیسکِ مشترک جمع نشود:sudo -u postgres psql -p 5440 -c "select pg_drop_replication_slot('zareian_247')"؛ روی ۲۴۷systemctl disable --now zareian-pg-tunnel zareian-pg-wal zareian-pg-basebackup.timer zareian-pg-export-events.timer zareian-applog-pull.timer. دیتابیسِ ۲۴۹ یک هفته برای برگشت میماند. - برگشت (اگر قدمِ ۵ خراب شد): origin در آروان دوباره ۲۴۹، و روی ۲۴۹
systemctl start zareian-mathapp. فقط تا وقتی درست است که ۲۱۶ نوشتنِ واقعی (پرداخت، ثبتنام) نگرفته؛ بعد از آن اول dumpِ ۲۱۶ به ۲۴۹. - اسناد: CLAUDE.md (سرورِ زنده = ۲۱۶؛ رویهی دیپلو با
zareian@78.38.35.216وsudo -u mathapp ./deploy.sh)، این بخش، وops/nginx/با مسیرهای/home/mathapp(۲۱۶ نسخهی sedشده را دارد).
بعد از سوئیچ
- یک هفته بعد، با تأییدِ صاحبِ محصول: پاک کردنِ بکاپهای رمزنگارینشدهی ۲۴۹ روی ۲۴۷ (
~/zareian-pgbackup، یونیتهایzareian-pg-*) و حذفِ_slot_state/OFFSITE_WAL_SLOTازconfig/health.pyوsettings.py. - لاگِ اپ: آرشیوِ شبانه روی ۲۱۶ هست (
zareian-applog-archive.timer) ولی هنوز به ۲۴۷ نمیرود؛ راهِ پیشنهادی: افزودنِlogs/archiveبهtadrisino-offsite-sync(همان rrsync، پوشهیapp-logs/). - ورود با کلیدِ SSH برای
zareian(صاحبِ محصول خواسته فعلاً بماند؛ روی لپتاپssh-keygen -t ed25519وssh-copy-id)، بعدPasswordAuthentication noباMatch Address 192.168.200.0/24برای ورودِ با رمز از شبکهی داخلی. - توکنِ فقطخواندنیِ Gitea برای
deploy.shروی ۲۱۶ (بعد از private شدنِ مخزن،git fetchبیرمز دیگر کار نمیکند). CORS_ALLOWED_ORIGINSدر.envِ سرور هنوزlocalhost:5174دارد؛ حذفش.- برنامهی صاحبِ محصول: مرورِ کاملِ یک فهرستِ امنیتیِ استاندارد (OWASP Top 10 / API Top 10 برای اپ، CIS Benchmark برای سرور، همراهِ
docs/security-before-production.md)، بعد لودتست روی ۲۱۶ (tools/loadtest/README.md؛ با هستهی بیشتر: ۸ worker وDB_POOL_MAX_SIZE6).
Qdrant
وضعِ فعلی Qdrant: بردارهای RAG روی https://qdrant.tadrisino2.ir هستند (پشت ArvanCloud؛ سرورِ اصلی از بیرون پیدا
نیست و در فهرستِ سرورهای ما نیست. rag_catalog آن را سرورِ تیمِ دیگر میداند). نسخهٔ 1.17.0، ۳۴ collection، حدود
۱۰۸۸۰ نقطه، همه با بُعد ۱۵۳۶ و Cosine.
هدف: همهٔ کتابهای ریاضیای که در Qdrant هست (ریاضی، حسابان، هندسه، آمار و احتمال، گسسته؛ پایههای سوم تا دوازدهم) روی یک Qdrant اختصاصی ببریم و اپ را به آن وصل کنیم، بدون ingest دوباره، چون ingest هزینهٔ GapGPT دارد.
کدام collectionها (۱۵ تا، حدود ۵۶۸۰ نقطه):
- ریاضی سوم، چهارم، پنجم، ششم، هفتم، هشتم، نهم، دهم.
- حسابان یازدهم، دوازدهم.
- هندسه دهم، یازدهم، دوازدهم.
- آمار و احتمال یازدهم.
- گسسته دوازدهم.
ریاضیِ سوم، ششم و هفتم را تیمِ دیگر ingest کرده، و صاحبِ محصول خواسته هر سه منتقل شوند. بقیهٔ collectionها (علوم، فارسی، مطالعات اجتماعی، هدیهٔ آسمان، پیامهای آسمان) در این برنامه نیستند.
اصولِ انتقال:
- از هر collection یک snapshot بگیرید، فایلِ آن را منتقل و روی Qdrant جدید بارگذاری کنید (بدون فراخوانیِ OpenAI).
- بعد از انتقال: تعدادِ نقاط، بُعد و Cosine یکی باشند؛ یک بردارِ ذخیرهشده را بهعنوانِ query بدهید و همان نقطه بالا بیاید.
/api/rag/ingestهنوز روی Qdrant فعلی مینویسد. در بازهٔ کپی ingest متوقف شود، یا بعد از سوئیچ نقاطِ تازه دوباره همگام شوند.QDRANT_URLوQDRANT_API_KEYرا فقط بعد از تأییدِ کپی عوض کنید؛ زودتر عوض شود، تیوتور جوابِ ناقص میدهد.
باز: محلِ سرورِ اصلیِ فعلیِ Qdrant (ادعای «روی سرورِ تیمِ دیگر» هنوز از روی سرور تأیید نشده). سرورِ جدید فعلاً به همان
qdrant.tadrisino2.ir وصل است.
لاگها
هر درخواستِ /api/ بهصورتِ یک شیءِ JSON در هر خط لاگ میشود
(method/path/status/duration/user — بدونِ بدنه، هدر یا سکرت) توسطِ
config.middleware.APIRequestLoggingMiddleware. خروجی روی stdout است و اگر
LOG_DIR ست باشد در app.log هم نوشته میشود.
هر خطای پیشبینینشده همیشه پاسخِ JSON برمیگرداند — نه صفحهی HTML پیشفرضِ
جنگو — و کامل با traceback لاگ میشود (config/exceptions.py).
پیش از آمدنِ کاربرِ واقعی فهرستِ
security-before-production.mdرا کامل ببندید (پنلِ ادمینِ عمومی، کدِ ورودِ بیهش، سقفِ پیامک، دسترسیِ مشترکِ سرور، …).