From 15583009e0aea8fc57365839364484cc120f24b2 Mon Sep 17 00:00:00 2001 From: mobiniqow Date: Wed, 16 Sep 2026 09:21:50 +0330 Subject: [PATCH] Add photo upload as a real onboarding step Photos are now step 2 of onboarding (right after basic info) instead of only being manageable from the profile page later. Uploading still goes through the existing /profile/photos endpoint; the new POST /onboarding/photos just confirms at least one photo exists and advances the step counter. Renumbered the OnboardingStep enum accordingly (9 steps total now). Co-Authored-By: Claude Sonnet 5 --- README.md | 23 ++++++++++++++--------- cmd/api/main.go | 2 +- internal/handler/onboarding_handler.go | 13 +++++++++++++ internal/models/user.go | 2 ++ internal/router/router.go | 1 + internal/service/onboarding_service.go | 24 +++++++++++++++++++++++- 6 files changed, 54 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index dedf434..b499cb7 100644 --- a/README.md +++ b/README.md @@ -78,23 +78,28 @@ func (s *KavenegarOTPSender) Send(phone, otp string) bool { ## فرآیند آنبوردینگ -۸ مرحله‌ی ثابت که باید به‌ترتیب طی بشن (نمی‌شه از روی مرحله پرید، ولی مراحل +۹ مرحله‌ی ثابت که باید به‌ترتیب طی بشن (نمی‌شه از روی مرحله پرید، ولی مراحل قبلی هر وقت بخوای قابل ویرایش‌اند؛ بعد از تکمیل کامل هم همه‌ی مراحل آزادانه -قابل ویرایش می‌شن): +قابل ویرایش می‌شن — حتی توی همون بازه‌ای که کاربر منتظر تایید ادمینه): | # | مرحله | Endpoint | |---|-------|----------| | ۱ | جنسیت، اسم، تاریخ تولد | `POST /onboarding/basic-info` | -| ۲ | قد و وزن | `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` | +| ۲ | عکس‌ها (حداقل ۱ عکس؛ خودِ آپلود از `/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` قرار می‌گیرد. diff --git a/cmd/api/main.go b/cmd/api/main.go index b73549e..5d11aad 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -60,7 +60,7 @@ func main() { userRepo, otpSender, jwtManager, cfg.OTPExpirySeconds, cfg.OTPLength, cfg.OTPHashSecret, ) - onboardingService := service.NewOnboardingService(userRepo, optionRepo, questionRepo) + onboardingService := service.NewOnboardingService(userRepo, optionRepo, questionRepo, photoRepo) adminService := service.NewAdminService(optionRepo, questionRepo, userRepo) profileService := service.NewProfileService(photoRepo, userRepo, photoStorage) discoveryService := service.NewDiscoveryService(userRepo, photoRepo, swipeRepo, matchRepo, optionRepo, questionRepo) diff --git a/internal/handler/onboarding_handler.go b/internal/handler/onboarding_handler.go index bcd6180..1c8c745 100644 --- a/internal/handler/onboarding_handler.go +++ b/internal/handler/onboarding_handler.go @@ -66,6 +66,19 @@ func (h *OnboardingHandler) BasicInfo(c *gin.Context) { utils.Success(c, http.StatusOK, "اطلاعات پایه ثبت شد", h.onboardingService.GetStatus(user)) } +// Photos POST /onboarding/photos — تایید مرحله‌ی عکس‌ها (خودِ آپلود از /profile/photos انجام می‌شه) +func (h *OnboardingHandler) Photos(c *gin.Context) { + user, ok := currentUser(c) + if !ok { + return + } + if err := h.onboardingService.ConfirmPhotos(user); err != nil { + h.handleStepError(c, err) + return + } + utils.Success(c, http.StatusOK, "عکس‌ها ثبت شد", h.onboardingService.GetStatus(user)) +} + // BodyInfo POST /onboarding/body-info func (h *OnboardingHandler) BodyInfo(c *gin.Context) { user, ok := currentUser(c) diff --git a/internal/models/user.go b/internal/models/user.go index ade426d..7488849 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -26,6 +26,7 @@ type OnboardingStep int const ( StepBasicInfo OnboardingStep = iota + 1 + StepPhotos StepBodyInfo StepProfileQuestions StepEducation @@ -58,6 +59,7 @@ var OnboardingStepOrder = []struct { Key string }{ {StepBasicInfo, "basic_info"}, + {StepPhotos, "photos"}, {StepBodyInfo, "body_info"}, {StepProfileQuestions, "profile_questions"}, {StepEducation, "education"}, diff --git a/internal/router/router.go b/internal/router/router.go index 8400ac8..02b93a8 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -59,6 +59,7 @@ func Setup( { onboarding.GET("/status", h.Onboarding.Status) onboarding.POST("/basic-info", h.Onboarding.BasicInfo) + onboarding.POST("/photos", h.Onboarding.Photos) onboarding.POST("/body-info", h.Onboarding.BodyInfo) onboarding.GET("/profile-questions", h.Onboarding.AvailableQuestions) onboarding.POST("/profile-questions", h.Onboarding.SetProfileQuestions) diff --git a/internal/service/onboarding_service.go b/internal/service/onboarding_service.go index 97b238c..adb75e2 100644 --- a/internal/service/onboarding_service.go +++ b/internal/service/onboarding_service.go @@ -17,23 +17,27 @@ var ( ErrSelectionCount = errors.New("تعداد گزینه‌های انتخابی مجاز نیست") ErrQuestionsInvalid = errors.New("سوالات انتخابی معتبر نیستند") ErrDuplicateQuestion = errors.New("هر سوال فقط یک‌بار می‌تواند انتخاب شود") + ErrNoPhotos = errors.New("برای ادامه حداقل باید یک عکس اضافه کنی") ) type OnboardingService struct { userRepo *repository.UserRepository optionRepo *repository.OptionRepository questionRepo *repository.QuestionRepository + photoRepo *repository.PhotoRepository } func NewOnboardingService( userRepo *repository.UserRepository, optionRepo *repository.OptionRepository, questionRepo *repository.QuestionRepository, + photoRepo *repository.PhotoRepository, ) *OnboardingService { return &OnboardingService{ userRepo: userRepo, optionRepo: optionRepo, questionRepo: questionRepo, + photoRepo: photoRepo, } } @@ -109,7 +113,25 @@ func (s *OnboardingService) SetBasicInfo(user *models.User, req dto.BasicInfoReq return s.advanceIfCurrent(user, models.StepBasicInfo) } -// ---------------- مرحله ۲: قد و وزن ---------------- +// ---------------- مرحله ۲: عکس‌ها (حداقل ۱ عکس) ---------------- + +// ConfirmPhotos عکس واقعی رو از طریق endpoint عمومی /profile/photos آپلود می‌کنیم؛ +// این متد فقط تایید می‌کنه که حداقل یک عکس هست و مرحله رو جلو می‌بره. +func (s *OnboardingService) ConfirmPhotos(user *models.User) error { + if err := s.canModifyStep(user, models.StepPhotos); err != nil { + return err + } + count, err := s.photoRepo.CountByUser(user.ID) + if err != nil { + return err + } + if count < 1 { + return ErrNoPhotos + } + return s.advanceIfCurrent(user, models.StepPhotos) +} + +// ---------------- مرحله ۳: قد و وزن ---------------- func (s *OnboardingService) SetBodyInfo(user *models.User, req dto.BodyInfoRequest) error { if err := s.canModifyStep(user, models.StepBodyInfo); err != nil {