chore: merge backend repo history into monorepo at backend/
git-subtree-dir: backend git-subtree-mainline:dc11190527git-subtree-split:acae976622
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
# اپلیکیشن دوستیابی — بکاند Go (Gin + GORM + PostgreSQL)
|
||||
|
||||
یک بکاند تمیز و لایهبندیشده برای یک اپ دوستیابی با:
|
||||
- احراز هویت فقط با شماره موبایل + کد OTP (بدون پسورد)
|
||||
- فرآیند آنبوردینگ مرحلهبهمرحله (۸ مرحله) برای تکمیل پروفایل
|
||||
- سیستم عمومی «دسته + گزینه» که کاملاً از طریق API ادمین قابل مدیریت است
|
||||
(بدون نیاز به تغییر کد برای اضافه/حذف گزینههای هر لیست)
|
||||
- سوالات پروفایل (Q&A) که ادمین تعریف میکند و کاربر ۳ تای آن را انتخاب میکند
|
||||
|
||||
## ساختار پروژه (لایهبندی تمیز)
|
||||
|
||||
```
|
||||
dating-app/
|
||||
├── cmd/api/main.go # نقطه ورود، wiring همهچیز
|
||||
├── internal/
|
||||
│ ├── config/ # خواندن تنظیمات از .env
|
||||
│ ├── database/ # اتصال دیتابیس، migrate، seed
|
||||
│ ├── models/ # مدلهای GORM
|
||||
│ ├── dto/ # ساختار request/response ها
|
||||
│ ├── repository/ # لایه دسترسی به داده (فقط query)
|
||||
│ ├── service/ # منطق تجاری (validation، state machine و...)
|
||||
│ ├── handler/ # هندلرهای HTTP (gin.Context)
|
||||
│ ├── middleware/ # auth، admin، cors
|
||||
│ └── router/ # تعریف روتها
|
||||
├── go.mod / go.sum
|
||||
└── .env.example
|
||||
```
|
||||
|
||||
لایهبندی مشابه الگوی selector/service که در پروژهی جنگویی قبلیتون بود:
|
||||
`repository` = selector (فقط query خام)، `service` = منطق و اعتبارسنجی،
|
||||
`handler` = فقط ترجمهی HTTP request/response.
|
||||
|
||||
## اجرا
|
||||
|
||||
### ۱) پیشنیاز: PostgreSQL
|
||||
|
||||
```bash
|
||||
createdb dating_app
|
||||
```
|
||||
|
||||
### ۲) تنظیمات
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# مقادیر رو مطابق محیط خودت ویرایش کن (خصوصاً JWT_*_SECRET و OTP_HASH_SECRET در پروداکشن)
|
||||
```
|
||||
|
||||
`ADMIN_BOOTSTRAP_PHONE` رو به شمارهی موبایل خودت تنظیم کن؛ در اولین اجرای برنامه
|
||||
یک کاربر ادمین با اون شماره ساخته میشه (یا اگه از قبل وجود داشت، ادمین میشه).
|
||||
بعدش با همون شماره از مسیر عادی OTP وارد شو تا توکن ادمین بگیری.
|
||||
|
||||
### ۳) اجرا
|
||||
|
||||
```bash
|
||||
go run ./cmd/api
|
||||
```
|
||||
|
||||
سرور روی `http://localhost:8080` بالا میاد. مسیر `/health` برای healthcheck هست.
|
||||
|
||||
## اتصال گیتوی پیامک واقعی
|
||||
|
||||
فعلاً `service.MockOTPSender` استفاده میشه که کد OTP رو بهجای ارسال واقعی توی
|
||||
لاگ چاپ میکنه (`internal/service/otp_sender.go`). برای وصل کردن یک گیتوی واقعی:
|
||||
|
||||
```go
|
||||
type KavenegarOTPSender struct{ apiKey string }
|
||||
|
||||
func (s *KavenegarOTPSender) Send(phone, otp string) bool {
|
||||
// درخواست HTTP به گیتوی، صبر برای پاسخ، و true/false واقعی برگردون.
|
||||
// این تابع باید SYNC باشه (نتیجهی واقعی رو برگردونه)، وگرنه کاربر حتی
|
||||
// در صورت شکست واقعی ارسال، پیام موفقیت میگیره.
|
||||
}
|
||||
```
|
||||
|
||||
و در `cmd/api/main.go` خط `otpSender := service.NewMockOTPSender(...)` رو با
|
||||
`otpSender := &KavenegarOTPSender{apiKey: cfg.KavenegarAPIKey}` عوض کن. بقیهی
|
||||
کد (`AuthService` و...) نیازی به تغییر نداره چون فقط با اینترفیس `OTPSender` کار میکنه.
|
||||
|
||||
## فرآیند آنبوردینگ
|
||||
|
||||
۹ مرحلهی ثابت که باید بهترتیب طی بشن (نمیشه از روی مرحله پرید، ولی مراحل
|
||||
قبلی هر وقت بخوای قابل ویرایشاند؛ بعد از تکمیل کامل هم همهی مراحل آزادانه
|
||||
قابل ویرایش میشن — حتی توی همون بازهای که کاربر منتظر تایید ادمینه):
|
||||
|
||||
| # | مرحله | Endpoint |
|
||||
|---|-------|----------|
|
||||
| ۱ | جنسیت، اسم، تاریخ تولد | `POST /onboarding/basic-info` |
|
||||
| ۲ | عکسها (حداقل ۱ عکس؛ خودِ آپلود از `/profile/photos`) | `POST /onboarding/photos` |
|
||||
| ۳ | قد و وزن | `POST /onboarding/body-info` |
|
||||
| ۴ | ۳ سوال از پیشتعریفشده + پاسخ | `POST /onboarding/profile-questions` |
|
||||
| ۵ | سطح تحصیلات + رشته | `POST /onboarding/education` |
|
||||
| ۶ | کارهایی که انجام میدهد (چندتایی) | `POST /onboarding/occupations` |
|
||||
| ۷ | ۳ چیز مهم در رابطه | `POST /onboarding/relationship-importance` |
|
||||
| ۸ | ۵ علاقهمندی | `POST /onboarding/interests` |
|
||||
| ۹ | دنبال چی میگردی | `POST /onboarding/looking-for` |
|
||||
|
||||
وضعیت فعلی: `GET /onboarding/status`
|
||||
|
||||
اگه کاربر قبلاً رد شده باشه و بعد از onboarded شدن هرکدوم از مراحل رو دوباره
|
||||
بزنه (حتی مرحلهی عکسها)، `approval_status` خودکار به `pending` برمیگرده تا
|
||||
ادمین دوباره بررسی کنه.
|
||||
|
||||
### بعد از تکمیل آنبوردینگ: تایید ادمین
|
||||
|
||||
به محض تکمیل مرحلهی ۸، `approval_status` کاربر روی `pending` قرار میگیرد.
|
||||
کاربر تا وقتی ادمین تاییدش نکنه (`approved`) اجازهی ورود به صفحهی دوستیابی
|
||||
رو نداره (این بخش در فاز بعدی پیاده میشه). اپ باید بعد از `is_onboarded=true`،
|
||||
مقدار `approval_status` رو از `GET /onboarding/status` یا `GET /profile/me`
|
||||
بخونه و بر اساسش یکی از این صفحات رو نشون بده:
|
||||
|
||||
| `approval_status` | صفحهی مناسب در اپ |
|
||||
|---|---|
|
||||
| `pending` | «در انتظار تایید ادمین» |
|
||||
| `approved` | ورود به صفحهی دوستیابی |
|
||||
| `rejected` | نمایش `rejection_reason` + امکان ویرایش پروفایل |
|
||||
|
||||
اگر کاربر رد شده باشه و بعداً هر کدوم از مراحل آنبوردینگ رو ویرایش کنه،
|
||||
`approval_status` خودکار به `pending` برمیگرده تا ادمین دوباره بررسی کنه.
|
||||
|
||||
گزینههای هر مرحله (که از پنل ادمین مدیریت میشن):
|
||||
`GET /onboarding/options/:category` با یکی از این category ها:
|
||||
`education_level`, `field_of_study`, `occupation`,
|
||||
`important_in_relationship`, `interest`, `looking_for`
|
||||
|
||||
سوالات قابلانتخاب برای مرحلهی ۳: `GET /onboarding/profile-questions`
|
||||
|
||||
### چرا این معماری؟
|
||||
|
||||
بهجای اینکه برای هر لیست (تحصیلات، علاقهمندی، اهداف رابطه و...) یک جدول
|
||||
جدا بسازیم، یک سیستم عمومی «دسته (Category) + گزینه (Option)» ساختیم.
|
||||
هر Category یک `min_select`/`max_select` داره (مثلاً علاقهمندیها دقیقاً ۵
|
||||
تا، تحصیلات دقیقاً ۱ تا، فعالیتها بدون سقف). این یعنی:
|
||||
- ادمین میتونه از طریق API هر وقت خواست گزینه اضافه/ویرایش/غیرفعال کنه،
|
||||
بدون نیاز به دیپلوی کد جدید.
|
||||
- اضافه کردن یک دستهی جدید در آینده فقط یک `INSERT` توی جدول `categories`
|
||||
میخواد، نه migration جدید.
|
||||
|
||||
## دوستیابی (Discovery)، مچ و پیامرسانی
|
||||
|
||||
بعد از اینکه کاربر تایید شد (`approval_status=approved`) وارد این بخش میشه.
|
||||
تشخیص جنسیت مقابل فعلاً **ساده و هترو-پیشفرض** هست (زن↔مرد)؛ سیستم هنوز
|
||||
فیلد جداگانهای برای «ترجیح جنسیتی/گرایش» نداره.
|
||||
|
||||
فاصله از روی `latitude`/`longitude` کاربر (که هیچوقت خام برنمیگرده، فقط
|
||||
فاصلهی محاسبهشده) با فرمول haversine حساب و به نزدیکترین کیلومتر گرد
|
||||
میشه — یعنی از همون ابتدا تقریبیه، نه GPS دقیق.
|
||||
|
||||
```
|
||||
GET /profile/photos (داخل /profile/me برمیگرده، endpoint جدا نداره)
|
||||
POST /profile/photos آپلود عکس multipart/form-data، فیلد "photo" (jpg/png/webp، حداکثر ۵ مگابایت، حداکثر ۶ عکس)
|
||||
DELETE /profile/photos/:id حذف عکس
|
||||
PATCH /profile/location body: {latitude, longitude}
|
||||
|
||||
GET /discover?max_distance_km=&min_age=&max_age= لیست پیشنهادها (نزدیکترین اول)
|
||||
GET /discover/:id پروفایل کامل یک کاندید
|
||||
POST /discover/:id/swipe body: {action: "like"|"pass"|"superlike"}
|
||||
اگه طرف مقابل هم قبلاً لایک کرده باشه، مچ ساخته میشه
|
||||
|
||||
GET /matches لیست مچها (با آخرین پیام هرکدوم)
|
||||
GET /matches/:id/messages پیامهای یک گفتگو
|
||||
POST /matches/:id/messages body: {text}
|
||||
```
|
||||
|
||||
فایلهای آپلودشده روی دیسک محلی سرور (`./uploads/photos`) ذخیره و از مسیر
|
||||
`/uploads/...` سرو میشن (پیادهسازی `service.PhotoStorage`؛ برای پروداکشن
|
||||
با یک object storage واقعی جایگزینش کن، دقیقاً مثل الگوی `OTPSender`).
|
||||
پیامرسانی فعلاً polling-based هست، نه realtime/websocket.
|
||||
|
||||
## API پنل ادمین
|
||||
|
||||
احراز هویت ادمین از همون مسیر OTP هست؛ فقط باید `is_admin=true` روی
|
||||
کاربر ست شده باشه (یا با `ADMIN_BOOTSTRAP_PHONE`، یا مستقیم توی دیتابیس).
|
||||
همهی مسیرهای زیر نیاز به هدر `Authorization: Bearer <access_token>` یک
|
||||
کاربر ادمین دارن.
|
||||
|
||||
```
|
||||
GET /admin/users/pending لیست کاربران آنبوردشده و منتظر بررسی (قدیمیترین اول)
|
||||
GET /admin/users/:id پروفایل کامل یک کاربر برای بررسی
|
||||
POST /admin/users/:id/approve تایید کاربر
|
||||
POST /admin/users/:id/reject رد کاربر body: {reason}
|
||||
|
||||
GET /admin/categories/:key دریافت تنظیمات یک دسته
|
||||
PATCH /admin/categories/:key ویرایش min_select/max_select/title
|
||||
|
||||
GET /admin/categories/:key/options لیست همه گزینههای یک دسته (فعال/غیرفعال)
|
||||
POST /admin/categories/:key/options ساخت گزینه جدید body: {title, sort_order}
|
||||
PATCH /admin/options/:id ویرایش گزینه body: {title?, sort_order?, is_active?}
|
||||
DELETE /admin/options/:id حذف گزینه
|
||||
|
||||
GET /admin/profile-questions لیست همه سوالات (فعال/غیرفعال)
|
||||
POST /admin/profile-questions ساخت سوال جدید body: {text, sort_order}
|
||||
PATCH /admin/profile-questions/:id ویرایش سوال body: {text?, sort_order?, is_active?}
|
||||
DELETE /admin/profile-questions/:id حذف سوال
|
||||
```
|
||||
|
||||
## خلاصهی کامل API
|
||||
|
||||
### احراز هویت (عمومی)
|
||||
```
|
||||
POST /api/v1/auth/request-otp body: {phone}
|
||||
POST /api/v1/auth/verify-otp body: {phone, otp} -> access/refresh token
|
||||
POST /api/v1/auth/token/refresh body: {refresh_token} -> access/refresh جدید
|
||||
```
|
||||
|
||||
### پروفایل
|
||||
```
|
||||
GET /api/v1/profile/me (نیاز به توکن) — پروفایل کامل + همهی انتخابها/پاسخها
|
||||
```
|
||||
|
||||
همهی پاسخها با این ساختار برمیگردن:
|
||||
```json
|
||||
{ "success": true, "message": "...", "data": { ... } }
|
||||
{ "success": false, "message": "...", "errors": { ... } }
|
||||
```
|
||||
|
||||
## نکاتی که از پروژهی جنگویی قبلیتون رعایت شده
|
||||
|
||||
- ارسال OTP **sync** انجام میشه (نه توی یک goroutine بیخیال از نتیجه)،
|
||||
چون قبلاً دیدیم نسخهی async باعث میشد کاربر با اینکه ارسال واقعاً شکست
|
||||
خورده، پیام موفقیت بگیره.
|
||||
- اگه ارسال پیامک واقعاً شکست بخوره، OTP ذخیرهشده پاک میشه تا کاربر
|
||||
فوراً بتونه دوباره تلاش کنه (وگرنه تا پایان OTP_EXPIRY_SECONDS با خطای
|
||||
«کد قبلی هنوز معتبره» قفل میمونه).
|
||||
- OTP هیچوقت خام ذخیره نمیشه؛ با HMAC-SHA256 هش میشه.
|
||||
- کد OTP فقط وقتی `APP_ENV != production` باشه توی لاگ چاپ میشه.
|
||||
Reference in New Issue
Block a user