chore: merge backend repo history into monorepo at backend/
git-subtree-dir: backend git-subtree-mainline:dc11190527git-subtree-split:acae976622
This commit is contained in:
@@ -0,0 +1,468 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user