# پیامک‌بان — بک‌اند بک‌اند 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` روی `/`. ## راه‌اندازی 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 ` دارند. بدنه‌ی `POST` این شکلی است: ```json { "contact_fields": [{"id": "...", "group_id": "...", "label": "شماره گذرنامه", "sort_order": 0}], "contacts": [{"id": "...", "name": "...", "phone": "...", "device_contact_id": null, "field_values": {"": "پشمک"}, "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//` | | GET/POST | `/api/banker/groups//fields/` | | GET/PATCH/DELETE | `/api/banker/fields//` | | GET/POST | `/api/banker/contacts/` | | GET/PATCH/DELETE | `/api/banker/contacts//` | | GET/POST | `/api/banker/templates/` | | GET/PATCH/DELETE | `/api/banker/templates//` | ### `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//` | وضعیت/گزارش یک ارسال گروهی | | POST | `/api/messaging//confirm//` | تایید ارسال دستی (بعد از تپ کاربر در واتساپ/تلگرام/بله/ایتا) | بدنه‌ی `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 ```