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:
@@ -0,0 +1,77 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"dating-app/internal/models"
|
||||
)
|
||||
|
||||
type MatchRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewMatchRepository(db *gorm.DB) *MatchRepository {
|
||||
return &MatchRepository{db: db}
|
||||
}
|
||||
|
||||
// normalizePair همیشه شناسهی کوچیکتر رو اول میذاره تا هر جفت فقط یک رکورد داشته باشه
|
||||
func normalizePair(a, b uint) (uint, uint) {
|
||||
if a < b {
|
||||
return a, b
|
||||
}
|
||||
return b, a
|
||||
}
|
||||
|
||||
// FindOrCreate اگه مچ بین این دو کاربر از قبل وجود داشته باشه همونو برمیگردونه،
|
||||
// وگرنه میسازتش. خروجی دوم true یعنی همین الان تازه ساخته شده.
|
||||
func (r *MatchRepository) FindOrCreate(userA, userB uint) (*models.Match, bool, error) {
|
||||
one, two := normalizePair(userA, userB)
|
||||
|
||||
var match models.Match
|
||||
err := r.db.Where("user_one_id = ? AND user_two_id = ?", one, two).First(&match).Error
|
||||
if err == nil {
|
||||
return &match, false, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
match = models.Match{UserOneID: one, UserTwoID: two}
|
||||
if err := r.db.Create(&match).Error; err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &match, true, nil
|
||||
}
|
||||
|
||||
func (r *MatchRepository) FindByID(id uint) (*models.Match, error) {
|
||||
var match models.Match
|
||||
if err := r.db.First(&match, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &match, nil
|
||||
}
|
||||
|
||||
// ListByUser مچهای کاربر رو از جدیدترین به قدیمیترین برمیگردونه
|
||||
func (r *MatchRepository) ListByUser(userID uint) ([]models.Match, error) {
|
||||
var matches []models.Match
|
||||
err := r.db.
|
||||
Where("user_one_id = ? OR user_two_id = ?", userID, userID).
|
||||
Order("created_at desc").
|
||||
Find(&matches).Error
|
||||
return matches, err
|
||||
}
|
||||
|
||||
// OtherUserID شناسهی طرف مقابل کاربر جاری رو توی یک مچ برمیگردونه
|
||||
func OtherUserID(match *models.Match, currentUserID uint) uint {
|
||||
if match.UserOneID == currentUserID {
|
||||
return match.UserTwoID
|
||||
}
|
||||
return match.UserOneID
|
||||
}
|
||||
|
||||
// IsMember یعنی این کاربر عضو این مچه
|
||||
func IsMember(match *models.Match, userID uint) bool {
|
||||
return match.UserOneID == userID || match.UserTwoID == userID
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"dating-app/internal/models"
|
||||
)
|
||||
|
||||
type MessageRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewMessageRepository(db *gorm.DB) *MessageRepository {
|
||||
return &MessageRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *MessageRepository) Create(msg *models.Message) error {
|
||||
return r.db.Create(msg).Error
|
||||
}
|
||||
|
||||
func (r *MessageRepository) ListByMatch(matchID uint) ([]models.Message, error) {
|
||||
var messages []models.Message
|
||||
err := r.db.Where("match_id = ?", matchID).Order("created_at asc").Find(&messages).Error
|
||||
return messages, err
|
||||
}
|
||||
|
||||
// LastMessage آخرین پیام یک مچ رو برمیگردونه (برای پیشنمایش لیست گفتگوها)
|
||||
func (r *MessageRepository) LastMessage(matchID uint) (*models.Message, error) {
|
||||
var msg models.Message
|
||||
err := r.db.Where("match_id = ?", matchID).Order("created_at desc").First(&msg).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &msg, nil
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"dating-app/internal/models"
|
||||
)
|
||||
|
||||
var ErrCategoryNotFound = errors.New("دستهی موردنظر یافت نشد")
|
||||
|
||||
type OptionRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewOptionRepository(db *gorm.DB) *OptionRepository {
|
||||
return &OptionRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *OptionRepository) GetCategoryByKey(key models.CategoryKey) (*models.Category, error) {
|
||||
var cat models.Category
|
||||
err := r.db.Where("key = ?", key).First(&cat).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cat, nil
|
||||
}
|
||||
|
||||
func (r *OptionRepository) UpdateCategory(cat *models.Category) error {
|
||||
return r.db.Save(cat).Error
|
||||
}
|
||||
|
||||
func (r *OptionRepository) ListActiveOptionsByCategoryKey(key models.CategoryKey) ([]models.Option, error) {
|
||||
cat, err := r.GetCategoryByKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var options []models.Option
|
||||
err = r.db.Where("category_id = ? AND is_active = ?", cat.ID, true).
|
||||
Order("sort_order asc, id asc").
|
||||
Find(&options).Error
|
||||
return options, err
|
||||
}
|
||||
|
||||
// ListOptionsByCategoryKeyAdmin تمام گزینهها (فعال و غیرفعال) رو برمیگردونه - برای پنل ادمین
|
||||
func (r *OptionRepository) ListOptionsByCategoryKeyAdmin(key models.CategoryKey) ([]models.Option, error) {
|
||||
cat, err := r.GetCategoryByKey(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var options []models.Option
|
||||
err = r.db.Where("category_id = ?", cat.ID).
|
||||
Order("sort_order asc, id asc").
|
||||
Find(&options).Error
|
||||
return options, err
|
||||
}
|
||||
|
||||
func (r *OptionRepository) CreateOption(opt *models.Option) error {
|
||||
return r.db.Create(opt).Error
|
||||
}
|
||||
|
||||
func (r *OptionRepository) FindOptionByID(id uint) (*models.Option, error) {
|
||||
var opt models.Option
|
||||
err := r.db.First(&opt, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &opt, nil
|
||||
}
|
||||
|
||||
func (r *OptionRepository) UpdateOption(opt *models.Option) error {
|
||||
return r.db.Save(opt).Error
|
||||
}
|
||||
|
||||
func (r *OptionRepository) DeleteOption(id uint) error {
|
||||
return r.db.Delete(&models.Option{}, id).Error
|
||||
}
|
||||
|
||||
// FindActiveOptionsByIDsInCategory فقط گزینههای فعالی که در این دسته هستند و id شون
|
||||
// داخل لیست دادهشده است رو برمیگردونه (برای اعتبارسنجی انتخاب کاربر)
|
||||
func (r *OptionRepository) FindActiveOptionsByIDsInCategory(categoryID uint, ids []uint) ([]models.Option, error) {
|
||||
var options []models.Option
|
||||
err := r.db.Where("category_id = ? AND is_active = ? AND id IN ?", categoryID, true, ids).
|
||||
Find(&options).Error
|
||||
return options, err
|
||||
}
|
||||
|
||||
// ReplaceUserSelections تمام انتخابهای قبلی کاربر در یک دسته را با لیست جدید جایگزین میکند (تراکنشی)
|
||||
func (r *OptionRepository) ReplaceUserSelections(userID, categoryID uint, optionIDs []uint) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("user_id = ? AND category_id = ?", userID, categoryID).
|
||||
Delete(&models.UserOptionSelection{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, optID := range optionIDs {
|
||||
sel := models.UserOptionSelection{
|
||||
UserID: userID,
|
||||
CategoryID: categoryID,
|
||||
OptionID: optID,
|
||||
}
|
||||
if err := tx.Create(&sel).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserSelections انتخابهای کاربر در یک دسته را همراه با اطلاعات گزینه برمیگرداند
|
||||
func (r *OptionRepository) GetUserSelections(userID, categoryID uint) ([]models.UserOptionSelection, error) {
|
||||
var selections []models.UserOptionSelection
|
||||
err := r.db.Preload("Option").
|
||||
Where("user_id = ? AND category_id = ?", userID, categoryID).
|
||||
Find(&selections).Error
|
||||
return selections, err
|
||||
}
|
||||
|
||||
// CountUserSelections تعداد انتخابهای ثبتشدهی کاربر در یک دسته را برمیگرداند
|
||||
// (برای بررسی اینکه مرحله تکمیل شده یا نه)
|
||||
func (r *OptionRepository) CountUserSelections(userID, categoryID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.UserOptionSelection{}).
|
||||
Where("user_id = ? AND category_id = ?", userID, categoryID).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"dating-app/internal/models"
|
||||
)
|
||||
|
||||
type PhotoRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPhotoRepository(db *gorm.DB) *PhotoRepository {
|
||||
return &PhotoRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *PhotoRepository) ListByUser(userID uint) ([]models.Photo, error) {
|
||||
var photos []models.Photo
|
||||
err := r.db.Where("user_id = ?", userID).Order("sort_order asc, id asc").Find(&photos).Error
|
||||
return photos, err
|
||||
}
|
||||
|
||||
func (r *PhotoRepository) ListByUserIDs(userIDs []uint) ([]models.Photo, error) {
|
||||
var photos []models.Photo
|
||||
if len(userIDs) == 0 {
|
||||
return photos, nil
|
||||
}
|
||||
err := r.db.Where("user_id IN ?", userIDs).Order("sort_order asc, id asc").Find(&photos).Error
|
||||
return photos, err
|
||||
}
|
||||
|
||||
func (r *PhotoRepository) CountByUser(userID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.Photo{}).Where("user_id = ?", userID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *PhotoRepository) Create(photo *models.Photo) error {
|
||||
return r.db.Create(photo).Error
|
||||
}
|
||||
|
||||
func (r *PhotoRepository) FindByID(id uint) (*models.Photo, error) {
|
||||
var photo models.Photo
|
||||
if err := r.db.First(&photo, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &photo, nil
|
||||
}
|
||||
|
||||
func (r *PhotoRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.Photo{}, id).Error
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
|
||||
"dating-app/internal/models"
|
||||
)
|
||||
|
||||
type QuestionRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewQuestionRepository(db *gorm.DB) *QuestionRepository {
|
||||
return &QuestionRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *QuestionRepository) ListActive() ([]models.ProfileQuestion, error) {
|
||||
var qs []models.ProfileQuestion
|
||||
err := r.db.Where("is_active = ?", true).Order("sort_order asc, id asc").Find(&qs).Error
|
||||
return qs, err
|
||||
}
|
||||
|
||||
func (r *QuestionRepository) ListAllAdmin() ([]models.ProfileQuestion, error) {
|
||||
var qs []models.ProfileQuestion
|
||||
err := r.db.Order("sort_order asc, id asc").Find(&qs).Error
|
||||
return qs, err
|
||||
}
|
||||
|
||||
func (r *QuestionRepository) FindByID(id uint) (*models.ProfileQuestion, error) {
|
||||
var q models.ProfileQuestion
|
||||
err := r.db.First(&q, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &q, nil
|
||||
}
|
||||
|
||||
func (r *QuestionRepository) FindActiveByIDs(ids []uint) ([]models.ProfileQuestion, error) {
|
||||
var qs []models.ProfileQuestion
|
||||
err := r.db.Where("is_active = ? AND id IN ?", true, ids).Find(&qs).Error
|
||||
return qs, err
|
||||
}
|
||||
|
||||
func (r *QuestionRepository) Create(q *models.ProfileQuestion) error {
|
||||
return r.db.Create(q).Error
|
||||
}
|
||||
|
||||
func (r *QuestionRepository) Update(q *models.ProfileQuestion) error {
|
||||
return r.db.Save(q).Error
|
||||
}
|
||||
|
||||
func (r *QuestionRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.ProfileQuestion{}, id).Error
|
||||
}
|
||||
|
||||
// ReplaceUserAnswers پاسخهای قبلی کاربر رو با پاسخهای جدید جایگزین میکنه (تراکنشی)
|
||||
func (r *QuestionRepository) ReplaceUserAnswers(userID uint, answers []models.UserProfileAnswer) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("user_id = ?", userID).Delete(&models.UserProfileAnswer{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range answers {
|
||||
answers[i].UserID = userID
|
||||
if err := tx.Create(&answers[i]).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *QuestionRepository) GetUserAnswers(userID uint) ([]models.UserProfileAnswer, error) {
|
||||
var answers []models.UserProfileAnswer
|
||||
err := r.db.Preload("Question").Where("user_id = ?", userID).Find(&answers).Error
|
||||
return answers, err
|
||||
}
|
||||
|
||||
func (r *QuestionRepository) CountUserAnswers(userID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.UserProfileAnswer{}).Where("user_id = ?", userID).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"dating-app/internal/models"
|
||||
)
|
||||
|
||||
type SwipeRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewSwipeRepository(db *gorm.DB) *SwipeRepository {
|
||||
return &SwipeRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *SwipeRepository) Create(swipe *models.Swipe) error {
|
||||
return r.db.Create(swipe).Error
|
||||
}
|
||||
|
||||
// HasLiked یعنی swiperID قبلاً swipedID رو لایک/سوپرلایک کرده
|
||||
func (r *SwipeRepository) HasLiked(swiperID, swipedID uint) (bool, error) {
|
||||
var swipe models.Swipe
|
||||
err := r.db.
|
||||
Where("swiper_id = ? AND swiped_id = ? AND action IN ?", swiperID, swipedID, []models.SwipeAction{models.SwipeLike, models.SwipeSuperlike}).
|
||||
First(&swipe).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *SwipeRepository) Exists(swiperID, swipedID uint) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.Swipe{}).Where("swiper_id = ? AND swiped_id = ?", swiperID, swipedID).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"dating-app/internal/models"
|
||||
)
|
||||
|
||||
type UserRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
||||
return &UserRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByPhone(phone string) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.Where("phone = ?", phone).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *UserRepository) FindByID(id uint) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.First(&user, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetOrCreateByPhone کاربر را با شمارهی دادهشده پیدا میکند یا در صورت نبود میسازد.
|
||||
func (r *UserRepository) GetOrCreateByPhone(phone string) (*models.User, bool, error) {
|
||||
var user models.User
|
||||
err := r.db.Where("phone = ?", phone).First(&user).Error
|
||||
if err == nil {
|
||||
return &user, false, nil
|
||||
}
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
user = models.User{
|
||||
Phone: phone,
|
||||
State: models.UserStateInactive,
|
||||
IsActive: true,
|
||||
CurrentStep: models.StepBasicInfo,
|
||||
}
|
||||
if err := r.db.Create(&user).Error; err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return &user, true, nil
|
||||
}
|
||||
|
||||
// ListPendingApproval کاربرانی که آنبوردینگ رو تکمیل کردن و منتظر بررسی ادمیناند
|
||||
// رو به ترتیب قدیمیترین اول برمیگردونه.
|
||||
func (r *UserRepository) ListPendingApproval() ([]models.User, error) {
|
||||
var users []models.User
|
||||
err := r.db.
|
||||
Where("is_onboarded = ? AND approval_status = ?", true, models.ApprovalPending).
|
||||
Order("updated_at asc").
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
// ListDiscoverable کاندیداهای بخش دوستیابی رو برمیگردونه: تاییدشده، فعال،
|
||||
// جنسیت مخالف کاربر (بهعنوان سادهترین فرض پیشفرض؛ فیلد ترجیح جنسیتی/گرایش
|
||||
// هنوز توی سیستم وجود نداره) و کسایی که قبلاً روشون سواپ نزده.
|
||||
// چون فاصله باید توی Go محاسبه بشه (haversine)، یک batch بزرگتر از حد
|
||||
// نهایی برمیگردونه تا بعد از فیلتر فاصله همچنان گزینهی کافی بمونه.
|
||||
func (r *UserRepository) ListDiscoverable(currentUserID uint, oppositeGender models.Gender, minAge, maxAge int, batchSize int) ([]models.User, error) {
|
||||
var users []models.User
|
||||
now := time.Now()
|
||||
minBirth := now.AddDate(-maxAge-1, 0, 1)
|
||||
maxBirth := now.AddDate(-minAge, 0, 0)
|
||||
|
||||
err := r.db.
|
||||
Where("id != ?", currentUserID).
|
||||
Where("is_onboarded = ? AND approval_status = ? AND is_active = ?", true, models.ApprovalApproved, true).
|
||||
Where("gender = ?", oppositeGender).
|
||||
Where("birth_date BETWEEN ? AND ?", minBirth, maxBirth).
|
||||
Where("NOT EXISTS (SELECT 1 FROM swipes s WHERE s.swiper_id = ? AND s.swiped_id = users.id)", currentUserID).
|
||||
Order("id asc").
|
||||
Limit(batchSize).
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *UserRepository) Save(user *models.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) UpdateFields(user *models.User, fields map[string]interface{}) error {
|
||||
return r.db.Model(user).Updates(fields).Error
|
||||
}
|
||||
|
||||
func (r *UserRepository) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
Reference in New Issue
Block a user