Files
2026-08-24 15:54:08 +03:30

208 lines
11 KiB
Markdown

# پیامک‌بان — بک‌اند
بک‌اند DRF برای اپ پیامک‌بان: احراز هویت با شماره موبایل + کد یک‌بارمصرف (OTP)
و توکن JWT، به‌علاوه یک اندپوینت پشتیبان‌گیری تا مخاطبین/فیلدهای دلخواه/
گروه‌ها/قالب‌های پیام کاربر با تعویض گوشی از دست نرود.
## پشته‌ی فنی
- Django 5.1 + Django REST Framework
- djangorestframework-simplejwt (JWT، با blacklist برای logout واقعی)
- PostgreSQL
- کد تایید (OTP) با HMAC-SHA256 هش می‌شود و هرگز متن ساده در دیتابیس نیست
— دقیقا مثل الگویی که در `apps/account` پروژه‌ی فروشگاه استفاده شده،
چون این ماژول پلاگین توکار ندارد؛ به‌جای پکیج `django-otp` (که برای
دستگاه‌های TOTP/Authenticator ساخته شده، نه ارسال پیامک با پترن)، همان
الگوی OTP دستی و امتحان‌پس‌داده‌ی پروژه‌ی مرجع را پیاده کردم. اگر واقعا
خودِ پکیج `django-otp` را می‌خواهی، بگو تا جایگزینش کنم.
## ساختار
```
apps/account/ احراز هویت (User با شماره موبایل، OTP، JWT)
apps/banker/ مخاطبین، فیلدهای دلخواه per-group، گروه‌ها، قالب‌های پیام (CRUD + بکاپ)
apps/messaging/ ارسال گروهی چندکاناله (پیامک/واتساپ/تلگرام/بله/ایتا)
apps/bizapps/ ریزاپ‌های کسب‌وکار کوچک: مشتریان، دفتر درآمد/هزینه، انبار، نوبت‌دهی
config/ تنظیمات، urls، wsgi/asgi
```
### `apps/bizapps` — ریزاپ‌های کسب‌وکار کوچک
چهار مدل مستقل، هرکدام CRUD ساده‌ی خودش را دارد (مثل الگوی `apps/banker`:
id همان UUID تولیدشده در اپ فلاتر، رکورد به کاربر صاحبش محدود):
| مسیر | مدل |
|---|---|
| `/api/bizapps/customers/` | `Customer` — مشتریان |
| `/api/bizapps/ledger/` | `LedgerEntry` — دفتر درآمد و هزینه (`entry_type`: income/expense) |
| `/api/bizapps/products/` | `Product` — انبار (`quantity`, `low_stock_threshold`) |
| `/api/bizapps/appointments/` | `Appointment` — نوبت‌دهی (`status`: scheduled/done/cancelled) |
هرکدام `GET/POST` روی مسیر بالا و `GET/PATCH/DELETE` روی `<id>/`.
## راه‌اندازی
1. یک دیتابیس PostgreSQL از قبل آماده کن (این پروژه فقط Dockerfile دارد، نه
docker-compose — یعنی پستگری را خودت جدا بالا می‌آوری یا از یک سرویس
موجود استفاده می‌کنی).
2. فایل env بساز:
```bash
cp .env.example .env
```
و مقداردهی کن — نکته‌های مهم:
- `SECRET_KEY`: `python -c "import secrets; print(secrets.token_urlsafe(50))"`
- `DB_*`: اطلاعات همان پستگری که در مرحله‌ی ۱ آماده کردی
- `SMS_PATTERN_NAME` و `SMS_TOKEN_URL`: طبق پایین توضیح داده شده
3. ساخت ایمیج و اجرا:
```bash
docker build -t sms-banker-backend .
docker run -d --name sms-banker-backend \
--env-file .env \
-p 8000:8000 \
sms-banker-backend
```
اگر پستگری هم داخل داکر است (کانتینر جدا، بدون compose)، هر دو کانتینر
را در یک شبکه‌ی مشترک بگذار تا `DB_HOST` با نام کانتینر پستگری کار کند:
```bash
docker network create sms-banker-net
docker run -d --name sms-banker-db --network sms-banker-net \
-e POSTGRES_DB=sms_banker -e POSTGRES_USER=... -e POSTGRES_PASSWORD=... \
postgres:16
docker run -d --name sms-banker-backend --network sms-banker-net \
--env-file .env -p 8000:8000 sms-banker-backend
# و در .env: DB_HOST=sms-banker-db
```
ورود/خروج، migrate و collectstatic خودکار در `entrypoint.sh` هنگام
استارت کانتینر انجام می‌شود.
## پیامک OTP — فقط دو مقدار از .env
طبق خواسته‌ات، ماژول ارسال پیامک (`apps/account/send_sms.py`) فقط دو مقدار
را از `.env` می‌خواند:
- `SMS_PATTERN_NAME`: نام/کد قالبی که نزد سرویس‌دهنده‌ی پیامکت ثبت شده
- `SMS_TOKEN_URL`: آدرسی که یک درخواست `POST` با بدنه‌ی
`{"pattern": ..., "receptor": "09xxxxxxxxx", "token": "12345"}` برایش
فرستاده می‌شود
اگر ساختار واقعی سرویس‌دهنده‌ات با این payload فرق دارد (مثلا نیاز به
هدر Authorization یا شکل دیگری از پارامترها دارد)، فقط تابع
`_call_token_url` در همان فایل را با مستندات سرویس‌دهنده‌ات هماهنگ کن؛
بقیه‌ی پروژه فقط `send_otp_message(phone, code)` را صدا می‌زند و از این
جزئیات خبر ندارد.
## اندپوینت‌ها
### `apps/account` — احراز هویت
| متد | مسیر | توضیح |
|---|---|---|
| POST | `/api/account/auth/request-otp/` | ارسال کد تایید (کاربر جدید هم خودکار ساخته می‌شود) |
| POST | `/api/account/auth/verify-otp/` | تایید کد، دریافت `access`/`refresh` |
| POST | `/api/account/auth/token/refresh/` | گرفتن access token جدید از refresh token |
| POST | `/api/account/auth/logout/` | باطل کردن refresh token (نیاز به access token) |
| GET | `/api/account/auth/profile/` | دریافت پروفایل |
| PATCH | `/api/account/auth/profile/` | ویرایش پروفایل (فعلا فقط `name`) |
| DELETE | `/api/account/auth/profile/delete/` | حذف حساب — نیاز به تایید OTP در بدنه‌ی درخواست |
### `apps/banker` — پشتیبان‌گیری داده‌های اپ
| متد | مسیر | توضیح |
|---|---|---|
| GET | `/api/banker/backup/` | آخرین نسخه‌ی ذخیره‌شده (برای بازیابی روی گوشی جدید) |
| POST | `/api/banker/backup/` | جایگزینی کامل داده‌ی کاربر با آنچه از اپ فرستاده می‌شود |
هر دو، نیاز به هدر `Authorization: Bearer <access token>` دارند. بدنه‌ی
`POST` این شکلی است:
```json
{
"contact_fields": [{"id": "...", "group_id": "...", "label": "شماره گذرنامه", "sort_order": 0}],
"contacts": [{"id": "...", "name": "...", "phone": "...", "device_contact_id": null, "field_values": {"<field_id>": "پشمک"}, "channel_ids": {"telegram": "123456"}}],
"groups": [{"id": "...", "name": "تور", "color": 4283215696, "is_general": false, "member_ids": ["..."]}],
"templates": [{"id": "...", "group_id": null, "name": "...", "body": "سلام {نام}", "mode": "merge"}]
}
```
`id`ها همان UUID تولیدشده در خودِ اپ فلاتر هستند — منبع حقیقت خودِ اپ روی
گوشی است، این اندپوینت فقط بکاپ/ریستور می‌کند، نه sync دوطرفه‌ی هوشمند.
فیلدهای دلخواه و قالب‌ها اکنون **گروه‌محور**اند: هر `ContactField` به یک
`ContactGroup` تعلق دارد (مثلا گروه «تور» → فیلد «شماره گذرنامه»)، و هر
`MessageTemplate` یا به یک گروه تعلق دارد یا `group_id: null` (قالب عمومی —
فقط فیلدهای پایه‌ی نام/شماره). هر کاربر دقیقاً یک گروه با `is_general: true`
دارد که خودکار ساخته می‌شود و قابل حذف نیست.
### CRUD تکی (علاوه بر بکاپ بالا)
برای عملیات تکی (نه جایگزینی کامل)، به‌جای backup:
| متد | مسیر |
|---|---|
| GET/POST | `/api/banker/groups/` |
| GET/PATCH/DELETE | `/api/banker/groups/<id>/` |
| GET/POST | `/api/banker/groups/<group_id>/fields/` |
| GET/PATCH/DELETE | `/api/banker/fields/<id>/` |
| GET/POST | `/api/banker/contacts/` |
| GET/PATCH/DELETE | `/api/banker/contacts/<id>/` |
| GET/POST | `/api/banker/templates/` |
| GET/PATCH/DELETE | `/api/banker/templates/<id>/` |
### `apps/messaging` — ارسال گروهی چندکاناله
فقط از مسیرهای رسمی هر پلتفرم استفاده می‌شود: پیامک بالک از طریق کاوه‌نگار
(خودکار، سمت سرور)، واتساپ از طریق WhatsApp Business Cloud API رسمی،
تلگرام/بله/ایتا از طریق Bot API رسمی — فقط برای مخاطبینی که قبلا opt-in
کرده‌اند (چت با بات را شروع کرده‌اند، `Contact.channel_ids`). هر کانالی که
پیکربندی/opt-in نداشته باشد، به‌جای خطا یا دور زدن ضدهرزنامه‌ی پلتفرم،
`needs_manual` برمی‌گرداند تا اپ یک صف deep-link یک‌لمسی به کاربر نشان دهد.
| متد | مسیر | توضیح |
|---|---|---|
| POST | `/api/messaging/send/` | ساخت + dispatch فوری یک ارسال گروهی |
| GET | `/api/messaging/<id>/` | وضعیت/گزارش یک ارسال گروهی |
| POST | `/api/messaging/<bulk_id>/confirm/<recipient_id>/` | تایید ارسال دستی (بعد از تپ کاربر در واتساپ/تلگرام/بله/ایتا) |
بدنه‌ی `POST /send/`:
```json
{
"group_id": "...",
"contact_ids": [],
"template_id": "...",
"body_override": "",
"channels": ["sms", "whatsapp", "telegram", "bale", "eitaa"]
}
```
`group_id` خالی = همه‌ی مخاطبین کاربر. یا `template_id` بده یا `body_override`
(متن آزاد بدون قالب). env varهای مربوط به هر کانال در `.env.example`
مستندند؛ تا وقتی تنظیم نشوند، همان کانال فقط `needs_manual` برمی‌گرداند.
## نکته‌ی مهم — بدون Postgres واقعی تست شده
با `manage.py check` و `manage.py makemigrations` (بدون اتصال واقعی به
Postgres) تایید شد که مدل‌ها/اپ‌ها درست بار می‌شوند و مهاجرت‌های اولیه
(`0001_initial`) برای هر سه اپ ساخته شدند — این‌ها الان در ریپو هستند، نیازی
به `makemigrations` دوباره نیست مگر مدلی عوض شود. در همین بررسی یک باگ
از قبل موجود هم پیدا و رفع شد: `ContactGroup.user` بدون `related_name`
صریح با فیلد `groups` توکار Django (`PermissionsMixin`) تداخل داشت؛ الان
`related_name="contact_groups"` است.
قبل از اجرای واقعی روی سیستم خودت با یک Postgres واقعی:
```bash
pip install -r requirements.txt
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
```