package service import ( "errors" "time" "dating-app/internal/dto" "dating-app/internal/models" "dating-app/internal/repository" ) var ( ErrUserNotOnboarded = errors.New("کاربر هنوز آنبوردینگ را تکمیل نکرده است") ErrBlockReasonEmpty = errors.New("دلیل بلاک کردن نمی‌تواند خالی باشد") ErrReportReasonEmpty = errors.New("دلیل گزارش نمی‌تواند خالی باشد") ) type AdminService struct { optionRepo *repository.OptionRepository questionRepo *repository.QuestionRepository userRepo *repository.UserRepository reportRepo *repository.ReportRepository matchRepo *repository.MatchRepository photoRepo *repository.PhotoRepository profileService *ProfileService } func NewAdminService( optionRepo *repository.OptionRepository, questionRepo *repository.QuestionRepository, userRepo *repository.UserRepository, reportRepo *repository.ReportRepository, matchRepo *repository.MatchRepository, photoRepo *repository.PhotoRepository, profileService *ProfileService, ) *AdminService { return &AdminService{ optionRepo: optionRepo, questionRepo: questionRepo, userRepo: userRepo, reportRepo: reportRepo, matchRepo: matchRepo, photoRepo: photoRepo, profileService: profileService, } } // ---------------- بررسی و تایید کاربران ---------------- // GetUser یک کاربر رو برای نمایش به ادمین (بررسی درخواست) برمی‌گردونه func (s *AdminService) GetUser(id uint) (*models.User, error) { return s.userRepo.FindByID(id) } // ListPendingUsers کاربرانی که آنبوردینگ رو تکمیل کردن و منتظر بررسی‌اند، به // همراه عکس‌هاشون (batch گرفته می‌شه، نه یک کوئری جدا به‌ازای هر کاربر) تا // ادمین بدون کلیک روی هرکدوم بتونه یه نگاه سریع به عکس‌ها بندازه. func (s *AdminService) ListPendingUsers() ([]models.User, map[uint][]string, error) { users, err := s.userRepo.ListPendingApproval() if err != nil { return nil, nil, err } ids := make([]uint, len(users)) for i, u := range users { ids[i] = u.ID } photos, err := s.photoRepo.ListByUserIDs(ids) if err != nil { return nil, nil, err } photosByUser := make(map[uint][]string, len(ids)) for _, p := range photos { photosByUser[p.UserID] = append(photosByUser[p.UserID], p.URL) } return users, photosByUser, nil } // ApproveUser کاربر رو تایید می‌کنه تا بتونه وارد صفحه‌ی دوستیابی بشه func (s *AdminService) ApproveUser(id uint) (*models.User, error) { user, err := s.userRepo.FindByID(id) if err != nil { return nil, err } if !user.IsOnboarded { return nil, ErrUserNotOnboarded } now := time.Now() updates := map[string]interface{}{ "approval_status": models.ApprovalApproved, "rejection_reason": nil, "reviewed_at": now, } if err := s.userRepo.UpdateFields(user, updates); err != nil { return nil, err } user.ApprovalStatus = models.ApprovalApproved user.RejectionReason = nil user.ReviewedAt = &now return user, nil } // RejectUser درخواست عضویت کاربر رو با یک دلیل مشخص رد می‌کنه func (s *AdminService) RejectUser(id uint, reason string) (*models.User, error) { user, err := s.userRepo.FindByID(id) if err != nil { return nil, err } if !user.IsOnboarded { return nil, ErrUserNotOnboarded } now := time.Now() updates := map[string]interface{}{ "approval_status": models.ApprovalRejected, "rejection_reason": reason, "reviewed_at": now, } if err := s.userRepo.UpdateFields(user, updates); err != nil { return nil, err } user.ApprovalStatus = models.ApprovalRejected user.RejectionReason = &reason user.ReviewedAt = &now return user, nil } // ---------------- همه‌ی کاربران، بلاک/آنبلاک، گزارش ---------------- // AdminUserListItem یک کاربر به‌همراه تعداد گزارش‌هاش، برای لیست کامل کاربران type AdminUserListItem struct { models.User ReportCount int64 `json:"report_count"` } // ListAllUsers همه‌ی کاربران رو صفحه‌بندی‌شده و قابل‌جستجو (شماره/اسم) برمی‌گردونه. func (s *AdminService) ListAllUsers(search string, page, pageSize int) ([]AdminUserListItem, int64, error) { if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 25 } users, total, err := s.userRepo.ListAll(search, pageSize, (page-1)*pageSize) if err != nil { return nil, 0, err } ids := make([]uint, len(users)) for i, u := range users { ids[i] = u.ID } counts, err := s.reportRepo.CountsByUserIDs(ids) if err != nil { return nil, 0, err } items := make([]AdminUserListItem, len(users)) for i, u := range users { items[i] = AdminUserListItem{User: u, ReportCount: counts[u.ID]} } return items, total, nil } // BlockUser از ورود کاربر به اپ جلوگیری می‌کنه (چه توکن معتبر داشته باشه چه نه). func (s *AdminService) BlockUser(id uint, reason string) (*models.User, error) { if reason == "" { return nil, ErrBlockReasonEmpty } user, err := s.userRepo.FindByID(id) if err != nil { return nil, err } now := time.Now() updates := map[string]interface{}{ "state": models.UserStateBlocked, "blocked_reason": reason, "blocked_at": now, } if err := s.userRepo.UpdateFields(user, updates); err != nil { return nil, err } user.State = models.UserStateBlocked user.BlockedReason = &reason user.BlockedAt = &now return user, nil } // UnblockUser دوباره اجازه‌ی ورود به کاربر می‌ده. func (s *AdminService) UnblockUser(id uint) (*models.User, error) { user, err := s.userRepo.FindByID(id) if err != nil { return nil, err } updates := map[string]interface{}{ "state": models.UserStateActive, "blocked_reason": nil, "blocked_at": nil, } if err := s.userRepo.UpdateFields(user, updates); err != nil { return nil, err } user.State = models.UserStateActive user.BlockedReason = nil user.BlockedAt = nil return user, nil } // ReportUser یک یادداشت نظارتی روی کاربر ثبت می‌کنه (مستقل از بلاک کردن). func (s *AdminService) ReportUser(id uint, reason string) (*models.Report, error) { if reason == "" { return nil, ErrReportReasonEmpty } if _, err := s.userRepo.FindByID(id); err != nil { return nil, err } report := &models.Report{UserID: id, Reason: reason} if err := s.reportRepo.Create(report); err != nil { return nil, err } return report, nil } // ReportPhoto یک عکس مشخص رو گزارش می‌کنه (روی صاحب عکس هم توی سابقه‌ی // گزارش‌ها ثبت می‌شه، با ارجاع به همون عکس). func (s *AdminService) ReportPhoto(photoID uint, reason string) (*models.Report, error) { if reason == "" { return nil, ErrReportReasonEmpty } photo, err := s.photoRepo.FindByID(photoID) if err != nil { return nil, err } report := &models.Report{UserID: photo.UserID, PhotoID: &photo.ID, Reason: reason} if err := s.reportRepo.Create(report); err != nil { return nil, err } return report, nil } // ListReports سابقه‌ی گزارش‌های ثبت‌شده روی یک کاربر رو برمی‌گردونه. func (s *AdminService) ListReports(userID uint) ([]models.Report, error) { return s.reportRepo.ListByUser(userID) } // ---------------- مچ‌ها (رفقا) ---------------- // AdminMatchView یک مچ به‌همراه اطلاعات خلاصه‌ی هر دو طرف، برای نمایش به ادمین. type AdminMatchView struct { ID uint `json:"id"` CreatedAt time.Time `json:"created_at"` UserOneID uint `json:"user_one_id"` UserOneName string `json:"user_one_name"` UserOnePhone string `json:"user_one_phone"` UserTwoID uint `json:"user_two_id"` UserTwoName string `json:"user_two_name"` UserTwoPhone string `json:"user_two_phone"` } // ListAllMatches همه‌ی مچ‌های اپ رو صفحه‌بندی‌شده برمی‌گردونه («رفقا»یی که با // هم مچ شدن)، جدیدترین اول. func (s *AdminService) ListAllMatches(page, pageSize int) ([]AdminMatchView, int64, error) { if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 25 } matches, total, err := s.matchRepo.ListAllPaginated(pageSize, (page-1)*pageSize) if err != nil { return nil, 0, err } userIDs := make([]uint, 0, len(matches)*2) for _, m := range matches { userIDs = append(userIDs, m.UserOneID, m.UserTwoID) } users, err := s.userRepo.FindByIDs(userIDs) if err != nil { return nil, 0, err } userByID := make(map[uint]models.User, len(users)) for _, u := range users { userByID[u.ID] = u } views := make([]AdminMatchView, 0, len(matches)) for _, m := range matches { one := userByID[m.UserOneID] two := userByID[m.UserTwoID] views = append(views, AdminMatchView{ ID: m.ID, CreatedAt: m.CreatedAt, UserOneID: one.ID, UserOneName: one.FirstName, UserOnePhone: one.Phone, UserTwoID: two.ID, UserTwoName: two.FirstName, UserTwoPhone: two.Phone, }) } return views, total, nil } // ---------------- نظارت بر عکس‌ها ---------------- // AdminPhotoView یک عکس آپلودشده به‌همراه اطلاعات صاحبش، برای نظارت ادمین. type AdminPhotoView struct { models.Photo OwnerName string `json:"owner_name"` OwnerPhone string `json:"owner_phone"` } // ListAllPhotos همه‌ی عکس‌های آپلودشده‌ی اپ رو صفحه‌بندی‌شده، جدیدترین اول برمی‌گردونه. func (s *AdminService) ListAllPhotos(page, pageSize int) ([]AdminPhotoView, int64, error) { if page < 1 { page = 1 } if pageSize < 1 || pageSize > 200 { pageSize = 50 } photos, total, err := s.photoRepo.ListAllPaginated(pageSize, (page-1)*pageSize) if err != nil { return nil, 0, err } ids := make([]uint, len(photos)) for i, p := range photos { ids[i] = p.UserID } users, err := s.userRepo.FindByIDs(ids) if err != nil { return nil, 0, err } userByID := make(map[uint]models.User, len(users)) for _, u := range users { userByID[u.ID] = u } views := make([]AdminPhotoView, len(photos)) for i, p := range photos { owner := userByID[p.UserID] views[i] = AdminPhotoView{Photo: p, OwnerName: owner.FirstName, OwnerPhone: owner.Phone} } return views, total, nil } // DeletePhotoAdmin یک عکس رو صرف‌نظر از مالکش حذف می‌کنه (برای نظارت محتوایی). func (s *AdminService) DeletePhotoAdmin(photoID uint) error { return s.profileService.DeletePhotoAdmin(photoID) } // ---------------- دسته‌ها و گزینه‌ها ---------------- func (s *AdminService) GetCategory(key models.CategoryKey) (*models.Category, error) { return s.optionRepo.GetCategoryByKey(key) } func (s *AdminService) UpdateCategory(key models.CategoryKey, req dto.UpdateCategoryRequest) (*models.Category, error) { cat, err := s.optionRepo.GetCategoryByKey(key) if err != nil { return nil, err } if req.Title != nil { cat.Title = *req.Title } if req.MinSelect != nil { cat.MinSelect = *req.MinSelect } if req.MaxSelect != nil { cat.MaxSelect = *req.MaxSelect } if err := s.optionRepo.UpdateCategory(cat); err != nil { return nil, err } return cat, nil } func (s *AdminService) ListOptions(key models.CategoryKey) ([]models.Option, error) { return s.optionRepo.ListOptionsByCategoryKeyAdmin(key) } func (s *AdminService) CreateOption(key models.CategoryKey, req dto.CreateOptionRequest) (*models.Option, error) { cat, err := s.optionRepo.GetCategoryByKey(key) if err != nil { return nil, err } opt := &models.Option{ CategoryID: cat.ID, Title: req.Title, SortOrder: req.SortOrder, IsActive: true, } if err := s.optionRepo.CreateOption(opt); err != nil { return nil, err } return opt, nil } func (s *AdminService) UpdateOption(id uint, req dto.UpdateOptionRequest) (*models.Option, error) { opt, err := s.optionRepo.FindOptionByID(id) if err != nil { return nil, err } if req.Title != nil { opt.Title = *req.Title } if req.SortOrder != nil { opt.SortOrder = *req.SortOrder } if req.IsActive != nil { opt.IsActive = *req.IsActive } if err := s.optionRepo.UpdateOption(opt); err != nil { return nil, err } return opt, nil } func (s *AdminService) DeleteOption(id uint) error { return s.optionRepo.DeleteOption(id) } // ---------------- سوالات پروفایل ---------------- func (s *AdminService) ListQuestions() ([]models.ProfileQuestion, error) { return s.questionRepo.ListAllAdmin() } func (s *AdminService) CreateQuestion(req dto.CreateProfileQuestionRequest) (*models.ProfileQuestion, error) { q := &models.ProfileQuestion{ Text: req.Text, SortOrder: req.SortOrder, IsActive: true, } if err := s.questionRepo.Create(q); err != nil { return nil, err } return q, nil } func (s *AdminService) UpdateQuestion(id uint, req dto.UpdateProfileQuestionRequest) (*models.ProfileQuestion, error) { q, err := s.questionRepo.FindByID(id) if err != nil { return nil, err } if req.Text != nil { q.Text = *req.Text } if req.SortOrder != nil { q.SortOrder = *req.SortOrder } if req.IsActive != nil { q.IsActive = *req.IsActive } if err := s.questionRepo.Update(q); err != nil { return nil, err } return q, nil } func (s *AdminService) DeleteQuestion(id uint) error { return s.questionRepo.Delete(id) }