chore: merge backend repo history into monorepo at backend/

git-subtree-dir: backend
git-subtree-mainline: dc11190527
git-subtree-split: acae976622
This commit is contained in:
2026-09-27 10:30:16 +03:30
67 changed files with 6835 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
package models
import "time"
// Photo یکی از عکس‌های پروفایل کاربر
type Photo struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"`
URL string `gorm:"size:500;not null" json:"url"`
SortOrder int `gorm:"default:0;not null" json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
}
func (Photo) TableName() string {
return "photos"
}
// SwipeAction نوع تصمیمی که کاربر روی یک پروفایل دیگه گرفته
type SwipeAction string
const (
SwipeLike SwipeAction = "like"
SwipeSuperlike SwipeAction = "superlike"
SwipePass SwipeAction = "pass"
)
// Swipe ثبت تصمیم یک کاربر (لایک/ردّ) روی یک کاربر دیگه.
// هر جفت (SwiperID, SwipedID) فقط یک بار می‌تونه ثبت بشه؛ توی discovery feed
// کسایی که قبلاً روشون تصمیم گرفته شده دیگه نشون داده نمی‌شن.
type Swipe struct {
ID uint `gorm:"primaryKey" json:"id"`
SwiperID uint `gorm:"not null;uniqueIndex:idx_swiper_swiped" json:"swiper_id"`
SwipedID uint `gorm:"not null;uniqueIndex:idx_swiper_swiped;index" json:"swiped_id"`
Action SwipeAction `gorm:"size:10;not null" json:"action"`
CreatedAt time.Time `json:"created_at"`
}
func (Swipe) TableName() string {
return "swipes"
}
// Match وقتی ساخته می‌شه که دو کاربر متقابلاً همدیگه رو لایک کرده باشن.
// UserOneID همیشه شناسه‌ی کوچیک‌تره (برای جلوگیری از رکورد تکراری در دو جهت).
type Match struct {
ID uint `gorm:"primaryKey" json:"id"`
UserOneID uint `gorm:"not null;uniqueIndex:idx_match_pair" json:"user_one_id"`
UserTwoID uint `gorm:"not null;uniqueIndex:idx_match_pair" json:"user_two_id"`
CreatedAt time.Time `json:"created_at"`
UserOne *User `gorm:"foreignKey:UserOneID" json:"-"`
UserTwo *User `gorm:"foreignKey:UserTwoID" json:"-"`
}
func (Match) TableName() string {
return "matches"
}
// Message یک پیام داخل یک مچ. از رمزنگاری سرتاسری (E2EE) استفاده می‌شه:
// سرور فقط CipherText+Nonce (خروجی رمزنگاری‌شده‌ی سمت کلاینت با NaCl box،
// X25519-XSalsa20-Poly1305) رو ذخیره می‌کنه و هیچ‌وقت متن خام پیام رو نمی‌بینه.
// Text قدیمی (قبل از اضافه شدن E2EE) برای نمایش پیام‌های تاریخی نگه داشته می‌شه؛
// پیام‌های جدید Text خالی دارن و به‌جاش CipherText/Nonce پر می‌شه.
type Message struct {
ID uint `gorm:"primaryKey" json:"id"`
MatchID uint `gorm:"not null;index" json:"match_id"`
SenderID uint `gorm:"not null" json:"sender_id"`
Text string `gorm:"size:2000;not null" json:"text"`
CipherText *string `gorm:"size:12000" json:"cipher_text,omitempty"`
Nonce *string `gorm:"size:64" json:"nonce,omitempty"`
ReadAt *time.Time `json:"read_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
func (Message) TableName() string {
return "messages"
}
+82
View File
@@ -0,0 +1,82 @@
package models
import "time"
// CategoryKey شناسه‌ی ثابت هر دسته‌ی لیست انتخابی.
// این کلیدها توی کد استفاده می‌شن (برای هر مرحله از آنبوردینگ)
// ولی گزینه‌های داخل هر دسته کاملاً از پنل ادمین قابل مدیریت هستن.
type CategoryKey string
const (
CategoryEducationLevel CategoryKey = "education_level"
CategoryFieldOfStudy CategoryKey = "field_of_study"
CategoryOccupation CategoryKey = "occupation"
CategoryImportantInRelationship CategoryKey = "important_in_relationship"
CategoryInterest CategoryKey = "interest"
CategoryLookingFor CategoryKey = "looking_for"
)
// AllCategoryKeys برای seed کردن و اعتبارسنجی
var AllCategoryKeys = []CategoryKey{
CategoryEducationLevel,
CategoryFieldOfStudy,
CategoryOccupation,
CategoryImportantInRelationship,
CategoryInterest,
CategoryLookingFor,
}
// Category یک دسته‌ی لیست انتخابی (مثلاً «علاقه‌مندی‌ها»).
// MinSelect/MaxSelect تعداد مجاز انتخاب رو مشخص می‌کنه (MaxSelect=0 یعنی نامحدود).
type Category struct {
ID uint `gorm:"primaryKey" json:"id"`
Key CategoryKey `gorm:"size:50;uniqueIndex;not null" json:"key"`
Title string `gorm:"size:150;not null" json:"title"`
// توجه: عمداً تگ gorm:"default:..." روی این دو فیلد گذاشته نشده.
// GORM مقادیر صفر (zero value) رو برای ستون‌هایی که تگ default دارن از
// INSERT حذف می‌کنه (چون نمی‌تونه "صفر عمدی" رو از "ست نشده" تشخیص بده)
// و این یعنی MaxSelect=0 (به‌معنی نامحدود) به‌جای ذخیره‌شدن، با دیفالت
// دیتابیس جایگزین می‌شد. چون همیشه این دو مقدار رو صریح ست می‌کنیم
// (در seed و در آپدیت ادمین)، نیازی به دیفالت دیتابیسی نیست.
MinSelect int `gorm:"not null" json:"min_select"`
MaxSelect int `gorm:"not null" json:"max_select"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Options []Option `gorm:"foreignKey:CategoryID" json:"options,omitempty"`
}
func (Category) TableName() string {
return "categories"
}
// Option یک آیتم قابل‌انتخاب داخل یک دسته (مدیریت‌شده توسط ادمین)
type Option struct {
ID uint `gorm:"primaryKey" json:"id"`
CategoryID uint `gorm:"not null;index" json:"category_id"`
Title string `gorm:"size:150;not null" json:"title"`
SortOrder int `gorm:"default:0;not null" json:"sort_order"`
IsActive bool `gorm:"default:true;not null" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Option) TableName() string {
return "options"
}
// UserOptionSelection انتخاب‌های کاربر از یک دسته‌ی مشخص.
// برای هر مرحله (مثل علاقه‌مندی‌ها) چند ردیف از این جدول ساخته می‌شه.
type UserOptionSelection struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;uniqueIndex:idx_user_category_option" json:"user_id"`
CategoryID uint `gorm:"not null;uniqueIndex:idx_user_category_option;index" json:"category_id"`
OptionID uint `gorm:"not null;uniqueIndex:idx_user_category_option" json:"option_id"`
CreatedAt time.Time `json:"created_at"`
Option *Option `gorm:"foreignKey:OptionID" json:"option,omitempty"`
}
func (UserOptionSelection) TableName() string {
return "user_option_selections"
}
@@ -0,0 +1,35 @@
package models
import "time"
// ProfileQuestion سوالاتی که ادمین از پیش در پنل وارد می‌کند
// (مثلاً «بزرگ‌ترین درسی که از زندگی گرفتی چیه؟») و کاربر باید
// دقیقاً ۳ تا از آن‌ها را انتخاب و برایش پاسخ بنویسد.
type ProfileQuestion struct {
ID uint `gorm:"primaryKey" json:"id"`
Text string `gorm:"size:255;not null" json:"text"`
SortOrder int `gorm:"default:0;not null" json:"sort_order"`
IsActive bool `gorm:"default:true;not null" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (ProfileQuestion) TableName() string {
return "profile_questions"
}
// UserProfileAnswer پاسخ کاربر به یکی از سوالات انتخابی
type UserProfileAnswer struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;uniqueIndex:idx_user_question" json:"user_id"`
QuestionID uint `gorm:"not null;uniqueIndex:idx_user_question" json:"question_id"`
Answer string `gorm:"type:text;not null" json:"answer"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Question *ProfileQuestion `gorm:"foreignKey:QuestionID" json:"question,omitempty"`
}
func (UserProfileAnswer) TableName() string {
return "user_profile_answers"
}
+17
View File
@@ -0,0 +1,17 @@
package models
import "time"
// Report یک یادداشت/پرچم نظارتی که ادمین روی یک کاربر (یا یک عکس مشخص از
// اون کاربر) ثبت می‌کنه؛ برای پیگیری سابقه‌ی نظارتی، مستقل از بلاک/حذف کردن.
type Report struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"`
PhotoID *uint `json:"photo_id,omitempty"`
Reason string `gorm:"type:text;not null" json:"reason"`
CreatedAt time.Time `json:"created_at"`
}
func (Report) TableName() string {
return "reports"
}
+134
View File
@@ -0,0 +1,134 @@
package models
import "time"
// Gender جنسیت کاربر
type Gender string
const (
GenderMale Gender = "male"
GenderFemale Gender = "female"
)
// UserState وضعیت کلی کاربر
type UserState string
const (
UserStateInactive UserState = "inactive"
UserStateActive UserState = "active"
UserStateBlocked UserState = "blocked"
)
// OnboardingStep مرحله‌ی فعلی کاربر در فرآیند تکمیل پروفایل.
// هر مقدار یعنی «این مرحله هنوز باید تکمیل بشه». وقتی به StepCompleted
// می‌رسه یعنی کاربر تمام مراحل رو طی کرده.
type OnboardingStep int
const (
StepBasicInfo OnboardingStep = iota + 1
StepPhotos
StepBodyInfo
StepProfileQuestions
StepEducation
StepOccupations
StepRelationshipImportance
StepInterests
StepLookingFor
StepCompleted
)
// ApprovalStatus وضعیت بررسی پروفایل توسط ادمین (بعد از تکمیل آنبوردینگ)
type ApprovalStatus string
const (
ApprovalPending ApprovalStatus = "pending"
ApprovalApproved ApprovalStatus = "approved"
ApprovalRejected ApprovalStatus = "rejected"
)
// StepInfo برای پاسخ‌های API که وضعیت هر مرحله رو توضیح می‌ده
type StepInfo struct {
Step OnboardingStep `json:"step"`
Key string `json:"key"`
Done bool `json:"done"`
}
// ordered لیست ترتیب مراحل آنبوردینگ (برای ساخت وضعیت کامل)
var OnboardingStepOrder = []struct {
Step OnboardingStep
Key string
}{
{StepBasicInfo, "basic_info"},
{StepPhotos, "photos"},
{StepBodyInfo, "body_info"},
{StepProfileQuestions, "profile_questions"},
{StepEducation, "education"},
{StepOccupations, "occupations"},
{StepRelationshipImportance, "relationship_importance"},
{StepInterests, "interests"},
{StepLookingFor, "looking_for"},
}
// User کاربر اصلی سیستم. احراز هویت فقط با شماره موبایل + OTP انجام می‌شود.
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Phone string `gorm:"size:11;uniqueIndex;not null" json:"phone"`
State UserState `gorm:"size:10;default:inactive;not null" json:"state"`
IsAdmin bool `gorm:"default:false;not null" json:"is_admin"`
IsActive bool `gorm:"default:true;not null" json:"is_active"`
// -------- بلاک شدن توسط ادمین --------
BlockedReason *string `json:"blocked_reason,omitempty"`
BlockedAt *time.Time `json:"blocked_at,omitempty"`
// فیلدهای OTP - هش می‌شوند و هرگز در پاسخ API برنمی‌گردند
OTPCode *string `gorm:"size:128" json:"-"`
OTPExpiry *time.Time `json:"-"`
// -------- اطلاعات پروفایل (پر می‌شوند طی فرآیند آنبوردینگ) --------
Gender *Gender `gorm:"size:10" json:"gender,omitempty"`
FirstName string `gorm:"size:100" json:"first_name,omitempty"`
BirthDate *time.Time `json:"birth_date,omitempty"`
HeightCM *int `json:"height_cm,omitempty"`
WeightKG *int `json:"weight_kg,omitempty"`
Bio *string `gorm:"size:300" json:"bio,omitempty"`
// -------- گیمیفیکیشن: استریک روزانه (چند روز پشت‌سرهم اپ باز شده) --------
StreakCount int `gorm:"default:0;not null" json:"streak_count"`
LastActiveDate *time.Time `json:"last_active_date,omitempty"`
// -------- ترجیحات اعلان --------
// فعلاً فقط ذخیره می‌شن (زیرساخت ارسال واقعی اعلان هنوز پیاده نشده)؛ وقتی
// اون سیستم اضافه شد، همین مقادیر رو برای تصمیم به ارسال یا نه می‌خونه.
NotifyMatches bool `gorm:"default:true;not null" json:"notify_matches"`
NotifyMessages bool `gorm:"default:true;not null" json:"notify_messages"`
NotifyLikes bool `gorm:"default:true;not null" json:"notify_likes"`
// -------- موقعیت مکانی (برای محاسبه‌ی فاصله‌ی تقریبی در دوستیابی) --------
Latitude *float64 `json:"-"`
Longitude *float64 `json:"-"`
// -------- کلید عمومی رمزنگاری سرتاسری پیام‌ها (E2EE) --------
// کلید خصوصی هیچ‌وقت به سرور نمی‌رسه و فقط روی دستگاه کاربر (localStorage)
// ذخیره می‌شه؛ سرور فقط این کلید عمومی رو نگه می‌داره تا طرف مقابل بتونه
// باهاش پیام رمزنگاری کنه. کلید X25519 هست، base64-encoded (۳۲ بایت خام).
PublicKey *string `gorm:"size:64" json:"public_key,omitempty"`
// -------- وضعیت آنبوردینگ --------
CurrentStep OnboardingStep `gorm:"default:1;not null" json:"current_step"`
IsOnboarded bool `gorm:"default:false;not null" json:"is_onboarded"`
// -------- بررسی ادمین (بعد از تکمیل آنبوردینگ) --------
// تا وقتی approved نشه، کاربر اجازه‌ی ورود به صفحه‌ی دوستیابی رو نداره.
ApprovalStatus ApprovalStatus `gorm:"size:10;default:pending;not null" json:"approval_status"`
RejectionReason *string `json:"rejection_reason,omitempty"`
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
LastLogin *time.Time `json:"last_login,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (User) TableName() string {
return "users"
}