Initial commit: dating app backend (Go/Gin/GORM/Postgres)

Phone+OTP auth, staged onboarding, admin approval workflow, and a
full discovery/swipe/match/chat system with photo upload and
approximate distance (haversine) based on user location.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 23:23:47 +03:30
commit 2823fb45f5
49 changed files with 4582 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
package database
import (
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"dating-app/internal/config"
"dating-app/internal/models"
)
// Connect به دیتابیس پستگرس وصل می‌شود
func Connect(cfg *config.Config) (*gorm.DB, error) {
logLevel := logger.Warn
if cfg.AppEnv == "development" {
logLevel = logger.Info
}
db, err := gorm.Open(postgres.Open(cfg.DSN()), &gorm.Config{
Logger: logger.Default.LogMode(logLevel),
})
if err != nil {
return nil, err
}
return db, nil
}
// AutoMigrate جدول‌های مورد نیاز را می‌سازد/به‌روزرسانی می‌کند
func AutoMigrate(db *gorm.DB) error {
return db.AutoMigrate(
&models.User{},
&models.Category{},
&models.Option{},
&models.UserOptionSelection{},
&models.ProfileQuestion{},
&models.UserProfileAnswer{},
&models.Photo{},
&models.Swipe{},
&models.Match{},
&models.Message{},
)
}
// SeedCategories دسته‌های ثابت لیست‌های انتخابی را در صورت نبود می‌سازد.
// خودِ گزینه‌های داخل هر دسته (به‌جز looking_for که صراحتاً در نیازمندی
// پروژه مشخص شده) باید از پنل ادمین اضافه شوند.
func SeedCategories(db *gorm.DB) error {
defaults := []models.Category{
{Key: models.CategoryEducationLevel, Title: "سطح تحصیلات", MinSelect: 1, MaxSelect: 1},
{Key: models.CategoryFieldOfStudy, Title: "رشته تحصیلی", MinSelect: 1, MaxSelect: 1},
{Key: models.CategoryOccupation, Title: "کارهایی که انجام می‌دهید", MinSelect: 1, MaxSelect: 0},
{Key: models.CategoryImportantInRelationship, Title: "برات توی یک رابطه مهمه", MinSelect: 3, MaxSelect: 3},
{Key: models.CategoryInterest, Title: "علاقه‌مندی‌ها", MinSelect: 5, MaxSelect: 5},
{Key: models.CategoryLookingFor, Title: "دنبال چی می‌گردی", MinSelect: 1, MaxSelect: 1},
}
for _, c := range defaults {
var existing models.Category
err := db.Where("key = ?", c.Key).First(&existing).Error
if err == gorm.ErrRecordNotFound {
if err := db.Create(&c).Error; err != nil {
return err
}
log.Printf("دسته‌ی «%s» ساخته شد", c.Title)
} else if err != nil {
return err
}
}
// گزینه‌های "دنبال چی می‌گردی" مطابق چیزی که در نیازمندی مشخص شده seed می‌شن
var lookingFor models.Category
if err := db.Where("key = ?", models.CategoryLookingFor).First(&lookingFor).Error; err == nil {
var count int64
db.Model(&models.Option{}).Where("category_id = ?", lookingFor.ID).Count(&count)
if count == 0 {
titles := []string{
"رابطه بلندمدت",
"همسر زندگی",
"قرارهای تفریحی",
"آشنایی با افراد جدید",
"ازدواج",
"دوست ساده",
}
for i, t := range titles {
db.Create(&models.Option{
CategoryID: lookingFor.ID,
Title: t,
SortOrder: i,
IsActive: true,
})
}
log.Println("گزینه‌های «دنبال چی می‌گردی» seed شدند")
}
}
return nil
}
// BootstrapAdmin اگر شماره‌ای برای ادمین اولیه مشخص شده باشد،
// یک کاربر ادمین با آن شماره می‌سازد (در صورت نبود).
func BootstrapAdmin(db *gorm.DB, phone string) error {
if phone == "" {
return nil
}
var user models.User
err := db.Where("phone = ?", phone).First(&user).Error
if err == gorm.ErrRecordNotFound {
user = models.User{
Phone: phone,
State: models.UserStateActive,
IsAdmin: true,
IsActive: true,
ApprovalStatus: models.ApprovalApproved,
}
if err := db.Create(&user).Error; err != nil {
return err
}
log.Printf("کاربر ادمین اولیه با شماره %s ساخته شد", phone)
return nil
} else if err != nil {
return err
}
if !user.IsAdmin {
user.IsAdmin = true
db.Save(&user)
log.Printf("کاربر %s به ادمین ارتقا یافت", phone)
}
return nil
}