208 lines
11 KiB
Markdown
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
|
||
|
|
```
|