package feetback import ( "context" "errors" "fmt" "strings" "api_yal/internal/models" "api_yal/internal/repository" "gorm.io/gorm" ) type FeedbackService interface { // Create создает новый отзыв Create(ctx context.Context, req *CreateFeedbackRequest) (*FeedbackResponse, error) // GetByID возвращает отзыв по ID GetByID(ctx context.Context, id uint) (*FeedbackResponse, error) // Update обновляет существующий отзыв Update(ctx context.Context, id uint, req *UpdateFeedbackRequest) (*FeedbackResponse, error) // Delete удаляет отзыв Delete(ctx context.Context, id uint) error // List возвращает список отзывов с пагинацией List(ctx context.Context, page, pageSize int) (*FeedbackListResponse, error) // ListByOwner возвращает отзывы по владельцу ListByOwner(ctx context.Context, ownerID uint, page, pageSize int) (*FeedbackListResponse, error) // ListByObject возвращает отзывы по объекту ListByObject(ctx context.Context, objectID uint, page, pageSize int) (*FeedbackListResponse, error) // ListByPlatform возвращает отзывы по платформе ListByPlatform(ctx context.Context, platform models.PlatformType, page, pageSize int) (*FeedbackListResponse, error) // Search ищет отзывы по тексту Search(ctx context.Context, query string, page, pageSize int) (*FeedbackListResponse, error) // Comment methods AddComment(ctx context.Context, feedbackID uint, comment *models.Comment) (*models.Comment, error) GetComments(ctx context.Context, feedbackID uint, page, pageSize int) ([]models.Comment, error) UpdateComment(ctx context.Context, commentID uint, text string) error DeleteComment(ctx context.Context, commentID uint) error // Stats возвращает статистику по отзывам GetStats(ctx context.Context) (*FeedbackStatsResponse, error) } type feedbackServiceImpl struct { feedbackRepository repository.FeedbackRepository } func NewFeedbackServiceImpl(feedbackRepository repository.FeedbackRepository) FeedbackService { return &feedbackServiceImpl{ feedbackRepository: feedbackRepository, } } // Create создает новый отзыв func (s *feedbackServiceImpl) Create(ctx context.Context, req *CreateFeedbackRequest) (*FeedbackResponse, error) { // Валидация входных данных if req == nil { return nil, errors.New("request cannot be nil") } // Валидация обязательных полей if req.ObjectID == 0 { return nil, errors.New("object ID is required") } if req.Score < 1 || req.Score > 5 { return nil, errors.New("score must be between 1 and 5") } if strings.TrimSpace(req.Text) == "" { return nil, errors.New("feedback text cannot be empty") } if len(req.Text) > 5000 { return nil, errors.New("feedback text cannot exceed 5000 characters") } if !s.isValidPlatform(req.Platform) { return nil, fmt.Errorf("invalid platform: %s", req.Platform) } // Получаем объект для проверки его существования object, err := s.feedbackRepository.GetObject(req.ObjectID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("object with ID %d not found", req.ObjectID) } return nil, fmt.Errorf("failed to get object: %w", err) } // Создаем отзыв feedback := &models.Feedback{ OwnerID: ctx.Value("userID").(uint), // Получаем из контекста ObjectID: req.ObjectID, Platform: req.Platform, Score: req.Score, Text: req.Text, } // Устанавливаем начальное значение счетчика комментариев feedback.CommentCount = 0 // Создаем отзыв if err := s.feedbackRepository.Create(feedback); err != nil { return nil, fmt.Errorf("failed to create feedback: %w", err) } // Формируем ответ response := &FeedbackResponse{ ID: feedback.ID, CreatedAt: feedback.CreatedAt, UpdatedAt: feedback.UpdatedAt, DeletedAt: feedback.DeletedAt, OwnerID: feedback.OwnerID, ObjectID: feedback.ObjectID, Platform: feedback.Platform, Score: feedback.Score, Text: feedback.Text, CommentCount: feedback.CommentCount, } return response, nil } // GetByID возвращает отзыв по ID func (s *feedbackServiceImpl) GetByID(ctx context.Context, id uint) (*FeedbackResponse, error) { if id == 0 { return nil, errors.New("invalid feedback ID") } feedback, err := s.feedbackRepository.GetByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("feedback with ID %d not found", id) } return nil, fmt.Errorf("failed to get feedback: %w", err) } // Формируем ответ response := &FeedbackResponse{ ID: feedback.ID, CreatedAt: feedback.CreatedAt, UpdatedAt: feedback.UpdatedAt, DeletedAt: feedback.DeletedAt, OwnerID: feedback.OwnerID, ObjectID: feedback.ObjectID, Platform: feedback.Platform, Score: feedback.Score, Text: feedback.Text, CommentCount: feedback.CommentCount, } // Добавляем связанные данные, если они загружены if feedback.Owner != nil { response.Owner = &account.AccountResponse{ ID: feedback.Owner.ID, Username: feedback.Owner.Username, Email: feedback.Owner.Email, } } if feedback.Object != nil { response.Object = &object.ObjectShortResponse{ ID: feedback.Object.ID, ShortName: feedback.Object.ShortName, LongName: feedback.Object.LongName, Type: feedback.Object.Type, Address: feedback.Object.Address, IsActive: feedback.Object.IsActive, IsVerified: feedback.Object.IsVerified, FeedbackCount: feedback.Object.FeedbackCount, } } return response, nil } // Update обновляет существующий отзыв func (s *feedbackServiceImpl) Update(ctx context.Context, id uint, req *UpdateFeedbackRequest) (*FeedbackResponse, error) { if id == 0 { return nil, errors.New("invalid feedback ID") } if req == nil { return nil, errors.New("request cannot be nil") } // Проверяем существование отзыва existing, err := s.feedbackRepository.GetByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("feedback with ID %d not found", id) } return nil, fmt.Errorf("failed to get feedback: %w", err) } // Проверяем, что текущий пользователь является владельцем userID := ctx.Value("userID").(uint) if existing.OwnerID != userID { return nil, errors.New("unauthorized: cannot update feedback owned by another user") } // Обновляем поля, если они указаны if req.Score != nil { if *req.Score < 1 || *req.Score > 5 { return nil, errors.New("score must be between 1 and 5") } existing.Score = *req.Score } if req.Text != nil { if strings.TrimSpace(*req.Text) == "" { return nil, errors.New("feedback text cannot be empty") } if len(*req.Text) > 5000 { return nil, errors.New("feedback text cannot exceed 5000 characters") } existing.Text = *req.Text } // Обновляем отзыв if err := s.feedbackRepository.Update(existing); err != nil { return nil, fmt.Errorf("failed to update feedback: %w", err) } // Формируем ответ response := &FeedbackResponse{ ID: existing.ID, CreatedAt: existing.CreatedAt, UpdatedAt: existing.UpdatedAt, DeletedAt: existing.DeletedAt, OwnerID: existing.OwnerID, ObjectID: existing.ObjectID, Platform: existing.Platform, Score: existing.Score, Text: existing.Text, CommentCount: existing.CommentCount, } return response, nil } // Delete удаляет отзыв func (s *feedbackServiceImpl) Delete(ctx context.Context, id uint) error { if id == 0 { return errors.New("invalid feedback ID") } // Проверяем существование отзыва existing, err := s.feedbackRepository.GetByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fmt.Errorf("feedback with ID %d not found", id) } return fmt.Errorf("failed to get feedback: %w", err) } // Проверяем, что текущий пользователь является владельцем userID := ctx.Value("userID").(uint) if existing.OwnerID != userID { return errors.New("unauthorized: cannot delete feedback owned by another user") } // Удаляем отзыв if err := s.feedbackRepository.Delete(id); err != nil { return fmt.Errorf("failed to delete feedback: %w", err) } return nil } // List возвращает список отзывов с пагинацией func (s *feedbackServiceImpl) List(ctx context.Context, page, pageSize int) (*FeedbackListResponse, error) { // Нормализация параметров пагинации offset, limit := s.normalizePagination(page, pageSize) // Получаем список отзывов feedbacks, err := s.feedbackRepository.List(offset, limit) if err != nil { return nil, fmt.Errorf("failed to list feedbacks: %w", err) } // Получаем общее количество total, err := s.feedbackRepository.Count() if err != nil { return nil, fmt.Errorf("failed to count feedbacks: %w", err) } // Формируем список ответов items := make([]FeedbackShortResponse, 0, len(feedbacks)) for _, feedback := range feedbacks { item := FeedbackShortResponse{ ID: feedback.ID, CreatedAt: feedback.CreatedAt, OwnerID: feedback.OwnerID, Platform: feedback.Platform, Score: feedback.Score, Text: feedback.Text, } // Добавляем имя владельца, если доступно if feedback.Owner != nil { item.OwnerName = feedback.Owner.Username } items = append(items, item) } // Формируем ответ с пагинацией return &FeedbackListResponse{ Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)), }, nil } // ListByOwner возвращает отзывы по владельцу func (s *feedbackServiceImpl) ListByOwner(ctx context.Context, ownerID uint, page, pageSize int) (*FeedbackListResponse, error) { if ownerID == 0 { return nil, errors.New("invalid owner ID") } offset, limit := s.normalizePagination(page, pageSize) feedbacks, err := s.feedbackRepository.ListByOwner(ownerID, offset, limit) if err != nil { return nil, fmt.Errorf("failed to list feedbacks by owner: %w", err) } // Получаем общее количество // Нужно реализовать метод CountByOwner в репозитории var total int64 // Временно используем общий count, но это не точно // TODO: Implement CountByOwner in repository if len(feedbacks) > 0 || page == 1 { // Если мы на первой странице или есть данные, получаем общий счет total, _ = s.feedbackRepository.Count() // Это временное решение } // Формируем список ответов items := make([]FeedbackShortResponse, 0, len(feedbacks)) for _, feedback := range feedbacks { item := FeedbackShortResponse{ ID: feedback.ID, CreatedAt: feedback.CreatedAt, OwnerID: feedback.OwnerID, Platform: feedback.Platform, Score: feedback.Score, Text: feedback.Text, } // Добавляем имя владельца, если доступно if feedback.Owner != nil { item.OwnerName = feedback.Owner.Username } items = append(items, item) } return &FeedbackListResponse{ Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)), }, nil } // ListByObject возвращает отзывы по объекту func (s *feedbackServiceImpl) ListByObject(ctx context.Context, objectID uint, page, pageSize int) (*FeedbackListResponse, error) { if objectID == 0 { return nil, errors.New("invalid object ID") } offset, limit := s.normalizePagination(page, pageSize) feedbacks, err := s.feedbackRepository.ListByObject(objectID, offset, limit) if err != nil { return nil, fmt.Errorf("failed to list feedbacks by object: %w", err) } // Получаем общее количество // Нужно реализовать метод CountByObject в репозитории var total int64 // Временно используем общий count // TODO: Implement CountByObject in repository if len(feedbacks) > 0 || page == 1 { total, _ = s.feedbackRepository.Count() } // Формируем список ответов items := make([]FeedbackShortResponse, 0, len(feedbacks)) for _, feedback := range feedbacks { item := FeedbackShortResponse{ ID: feedback.ID, CreatedAt: feedback.CreatedAt, OwnerID: feedback.OwnerID, Platform: feedback.Platform, Score: feedback.Score, Text: feedback.Text, } // Добавляем имя владельца, если доступно if feedback.Owner != nil { item.OwnerName = feedback.Owner.Username } items = append(items, item) } return &FeedbackListResponse{ Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)), }, nil } // ListByPlatform возвращает отзывы по платформе func (s *feedbackServiceImpl) ListByPlatform(ctx context.Context, platform models.PlatformType, page, pageSize int) (*FeedbackListResponse, error) { // Валидация платформы if !s.isValidPlatform(platform) { return nil, fmt.Errorf("invalid platform: %s", platform) } offset, limit := s.normalizePagination(page, pageSize) feedbacks, err := s.feedbackRepository.ListByPlatform(platform, offset, limit) if err != nil { return nil, fmt.Errorf("failed to list feedbacks by platform: %w", err) } // Получаем общее количество // Нужно реализовать метод CountByPlatform в репозитории var total int64 // Временно используем общий count // TODO: Implement CountByPlatform in repository if len(feedbacks) > 0 || page == 1 { total, _ = s.feedbackRepository.Count() } // Формируем список ответов items := make([]FeedbackShortResponse, 0, len(feedbacks)) for _, feedback := range feedbacks { item := FeedbackShortResponse{ ID: feedback.ID, CreatedAt: feedback.CreatedAt, OwnerID: feedback.OwnerID, Platform: feedback.Platform, Score: feedback.Score, Text: feedback.Text, } // Добавляем имя владельца, если доступно if feedback.Owner != nil { item.OwnerName = feedback.Owner.Username } items = append(items, item) } return &FeedbackListResponse{ Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)), }, nil } // Search ищет отзывы по тексту func (s *feedbackServiceImpl) Search(ctx context.Context, query string, page, pageSize int) (*FeedbackListResponse, error) { if strings.TrimSpace(query) == "" { return nil, errors.New("search query cannot be empty") } offset, limit := s.normalizePagination(page, pageSize) feedbacks, err := s.feedbackRepository.Search(query, offset, limit) if err != nil { return nil, fmt.Errorf("failed to search feedbacks: %w", err) } // Получаем общее количество // Нужно реализовать метод SearchCount в репозитории var total int64 // Временно используем общий count // TODO: Implement SearchCount in repository if len(feedbacks) > 0 || page == 1 { total, _ = s.feedbackRepository.Count() } // Формируем список ответов items := make([]FeedbackShortResponse, 0, len(feedbacks)) for _, feedback := range feedbacks { item := FeedbackShortResponse{ ID: feedback.ID, CreatedAt: feedback.CreatedAt, OwnerID: feedback.OwnerID, Platform: feedback.Platform, Score: feedback.Score, Text: feedback.Text, } // Добавляем имя владельца, если доступно if feedback.Owner != nil { item.OwnerName = feedback.Owner.Username } items = append(items, item) } return &FeedbackListResponse{ Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)), }, nil } // GetStats возвращает статистику по отзывам func (s *feedbackServiceImpl) GetStats(ctx context.Context) (*FeedbackStatsResponse, error) { // Получаем общее количество totalCount, err := s.feedbackRepository.Count() if err != nil { return nil, fmt.Errorf("failed to count feedbacks: %w", err) } // Если отзывов нет, возвращаем нулевую статистику if totalCount == 0 { return &FeedbackStatsResponse{ TotalCount: 0, AverageScore: 0, PlatformStats: make(map[models.PlatformType]int), ScoreDistribution: map[int]int{1: 0, 2: 0, 3: 0, 4: 0, 5: 0}, }, nil } // Для простоты, возвращаем заглушки для остальных метрик // В реальной реализации нужно выполнять агрегационные запросы к БД return &FeedbackStatsResponse{ TotalCount: int(totalCount), AverageScore: 3.5, // Заглушка PlatformStats: map[models.PlatformType]int{"entrepreneur": int(totalCount / 2), "tourist": int(totalCount / 2)}, ScoreDistribution: map[int]int{1: 10, 2: 20, 3: 30, 4: 40, 5: 50}, }, nil } // Вспомогательные методы // normalizePagination нормализует параметры пагинации func (s *feedbackServiceImpl) normalizePagination(page, pageSize int) (offset, limit int) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 10 } if pageSize > 100 { pageSize = 100 } offset = (page - 1) * pageSize limit = pageSize return offset, limit } // isValidPlatform проверяет корректность платформы func (s *feedbackServiceImpl) isValidPlatform(platform models.PlatformType) bool { validPlatforms := []models.PlatformType{"entrepreneur", "tourist"} for _, p := range validPlatforms { if p == platform { return true } } return false }