Files

469 lines
14 KiB
Go
Raw Permalink Normal View History

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)
}