From 42549eb116378977421eabecc1befeb37d48ba51 Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Tue, 19 May 2026 14:16:43 +0500 Subject: [PATCH] last --- .../internal/domain/feetback/handler.go | 144 +++++++++++---- .../internal/domain/feetback/service.go | 167 ++++++++++++++++++ .../repository/feedback_repository.go | 25 ++- .../repository/feedback_repository_impl.go | 126 ++++++++++++- 4 files changed, 423 insertions(+), 39 deletions(-) diff --git a/main_dc/yalarba/api_yal/internal/domain/feetback/handler.go b/main_dc/yalarba/api_yal/internal/domain/feetback/handler.go index c5c827e..45677d0 100644 --- a/main_dc/yalarba/api_yal/internal/domain/feetback/handler.go +++ b/main_dc/yalarba/api_yal/internal/domain/feetback/handler.go @@ -6,6 +6,7 @@ import ( "strconv" "api_yal/internal/logger" + "api_yal/internal/middleware" "api_yal/internal/models" "github.com/go-chi/chi/v5" @@ -24,21 +25,22 @@ func NewFeedbackHandler(service FeedbackService) *FeedbackHandler { // CreateFeedback создает новый отзыв func (h *FeedbackHandler) CreateFeedback(w http.ResponseWriter, r *http.Request) { - var feedback models.Feedback - if err := json.NewDecoder(r.Body).Decode(&feedback); err != nil { + var req CreateFeedbackRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } - if err := h.service.Create(r.Context(), &feedback); err != nil { + response, err := h.service.Create(r.Context(), &req) + if err != nil { logger.Get().Error("Failed to create feedback", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(feedback) + json.NewEncoder(w).Encode(response) } // GetFeedbackByID возвращает отзыв по ID @@ -50,7 +52,7 @@ func (h *FeedbackHandler) GetFeedbackByID(w http.ResponseWriter, r *http.Request return } - feedback, err := h.service.GetByID(r.Context(), uint(id)) + response, err := h.service.GetByID(r.Context(), uint(id)) if err != nil { logger.Get().Error("Failed to get feedback", zap.Error(err)) http.Error(w, err.Error(), http.StatusNotFound) @@ -58,7 +60,7 @@ func (h *FeedbackHandler) GetFeedbackByID(w http.ResponseWriter, r *http.Request } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(feedback) + json.NewEncoder(w).Encode(response) } // UpdateFeedback обновляет существующий отзыв @@ -70,21 +72,21 @@ func (h *FeedbackHandler) UpdateFeedback(w http.ResponseWriter, r *http.Request) return } - var feedback models.Feedback - if err := json.NewDecoder(r.Body).Decode(&feedback); err != nil { + var req UpdateFeedbackRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } - feedback.ID = uint(id) - if err := h.service.Update(r.Context(), &feedback); err != nil { + response, err := h.service.Update(r.Context(), uint(id), &req) + if err != nil { logger.Get().Error("Failed to update feedback", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(feedback) + json.NewEncoder(w).Encode(response) } // DeleteFeedback удаляет отзыв @@ -98,7 +100,7 @@ func (h *FeedbackHandler) DeleteFeedback(w http.ResponseWriter, r *http.Request) if err := h.service.Delete(r.Context(), uint(id)); err != nil { logger.Get().Error("Failed to delete feedback", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -120,7 +122,7 @@ func (h *FeedbackHandler) ListFeedbacks(w http.ResponseWriter, r *http.Request) pageSize = 100 } - feedbacks, total, err := h.service.List(r.Context(), page, pageSize) + response, err := h.service.List(r.Context(), page, pageSize) if err != nil { logger.Get().Error("Failed to list feedbacks", zap.Error(err)) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -128,15 +130,16 @@ func (h *FeedbackHandler) ListFeedbacks(w http.ResponseWriter, r *http.Request) } w.Header().Set("Content-Type", "application/json") - w.Header().Set("X-Total-Count", strconv.FormatInt(total, 10)) - json.NewEncoder(w).Encode(feedbacks) + json.NewEncoder(w).Encode(response) } // GetMyFeedbacks возвращает отзывы текущего пользователя func (h *FeedbackHandler) GetMyFeedbacks(w http.ResponseWriter, r *http.Request) { - // Здесь нужно получить ownerID из JWT токена - // Для примера используем заглушку - ownerID := uint(1) // TODO: Get from context + userID, ok := middleware.GetUserID(r.Context()) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } page, _ := strconv.Atoi(r.URL.Query().Get("page")) pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size")) @@ -147,8 +150,11 @@ func (h *FeedbackHandler) GetMyFeedbacks(w http.ResponseWriter, r *http.Request) if pageSize <= 0 { pageSize = 10 } + if pageSize > 100 { + pageSize = 100 + } - feedbacks, err := h.service.ListByOwner(r.Context(), ownerID, page, pageSize) + response, err := h.service.ListByOwner(r.Context(), userID, page, pageSize) if err != nil { logger.Get().Error("Failed to get user feedbacks", zap.Error(err)) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -156,7 +162,7 @@ func (h *FeedbackHandler) GetMyFeedbacks(w http.ResponseWriter, r *http.Request) } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(feedbacks) + json.NewEncoder(w).Encode(response) } // GetFeedbacksByObject возвращает отзывы по объекту @@ -170,8 +176,18 @@ func (h *FeedbackHandler) GetFeedbacksByObject(w http.ResponseWriter, r *http.Re page, _ := strconv.Atoi(r.URL.Query().Get("page")) pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size")) + + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + if pageSize > 100 { + pageSize = 100 + } - feedbacks, err := h.service.ListByObject(r.Context(), uint(objectID), page, pageSize) + response, err := h.service.ListByObject(r.Context(), uint(objectID), page, pageSize) if err != nil { logger.Get().Error("Failed to get object feedbacks", zap.Error(err)) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -179,7 +195,7 @@ func (h *FeedbackHandler) GetFeedbacksByObject(w http.ResponseWriter, r *http.Re } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(feedbacks) + json.NewEncoder(w).Encode(response) } // GetFeedbacksByPlatform возвращает отзывы по платформе @@ -189,8 +205,18 @@ func (h *FeedbackHandler) GetFeedbacksByPlatform(w http.ResponseWriter, r *http. page, _ := strconv.Atoi(r.URL.Query().Get("page")) pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size")) + + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + if pageSize > 100 { + pageSize = 100 + } - feedbacks, err := h.service.ListByPlatform(r.Context(), platform, page, pageSize) + response, err := h.service.ListByPlatform(r.Context(), platform, page, pageSize) if err != nil { logger.Get().Error("Failed to get platform feedbacks", zap.Error(err)) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -198,7 +224,7 @@ func (h *FeedbackHandler) GetFeedbacksByPlatform(w http.ResponseWriter, r *http. } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(feedbacks) + json.NewEncoder(w).Encode(response) } // SearchFeedbacks ищет отзывы по тексту @@ -211,8 +237,18 @@ func (h *FeedbackHandler) SearchFeedbacks(w http.ResponseWriter, r *http.Request page, _ := strconv.Atoi(r.URL.Query().Get("page")) pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size")) + + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + if pageSize > 100 { + pageSize = 100 + } - feedbacks, err := h.service.Search(r.Context(), query, page, pageSize) + response, err := h.service.Search(r.Context(), query, page, pageSize) if err != nil { logger.Get().Error("Failed to search feedbacks", zap.Error(err)) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -220,7 +256,20 @@ func (h *FeedbackHandler) SearchFeedbacks(w http.ResponseWriter, r *http.Request } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(feedbacks) + json.NewEncoder(w).Encode(response) +} + +// GetFeedbackStats возвращает статистику по отзывам +func (h *FeedbackHandler) GetFeedbackStats(w http.ResponseWriter, r *http.Request) { + stats, err := h.service.GetStats(r.Context()) + if err != nil { + logger.Get().Error("Failed to get feedback stats", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(stats) } // GetFeedbackComments возвращает комментарии к отзыву @@ -234,16 +283,34 @@ func (h *FeedbackHandler) GetFeedbackComments(w http.ResponseWriter, r *http.Req page, _ := strconv.Atoi(r.URL.Query().Get("page")) pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size")) + + if page <= 0 { + page = 1 + } + if pageSize <= 0 { + pageSize = 10 + } + if pageSize > 100 { + pageSize = 100 + } - comments, err := h.service.GetComments(r.Context(), uint(id), page, pageSize) + comments, total, err := h.service.GetComments(r.Context(), uint(id), page, pageSize) if err != nil { logger.Get().Error("Failed to get comments", zap.Error(err)) http.Error(w, err.Error(), http.StatusInternalServerError) return } + response := map[string]interface{}{ + "items": comments, + "total": total, + "page": page, + "page_size": pageSize, + "total_pages": int((total + int64(pageSize) - 1) / int64(pageSize)), + } + w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(comments) + json.NewEncoder(w).Encode(response) } // AddComment добавляет комментарий к отзыву @@ -255,15 +322,18 @@ func (h *FeedbackHandler) AddComment(w http.ResponseWriter, r *http.Request) { return } - var comment models.Comment - if err := json.NewDecoder(r.Body).Decode(&comment); err != nil { + var req struct { + Text string `json:"text" binding:"required"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return } - if err := h.service.AddComment(r.Context(), uint(id), &comment); err != nil { + comment, err := h.service.AddComment(r.Context(), uint(id), req.Text) + if err != nil { logger.Get().Error("Failed to add comment", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -282,7 +352,7 @@ func (h *FeedbackHandler) UpdateComment(w http.ResponseWriter, r *http.Request) } var req struct { - Text string `json:"text"` + Text string `json:"text" binding:"required"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) @@ -291,7 +361,7 @@ func (h *FeedbackHandler) UpdateComment(w http.ResponseWriter, r *http.Request) if err := h.service.UpdateComment(r.Context(), uint(commentID), req.Text); err != nil { logger.Get().Error("Failed to update comment", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } @@ -310,7 +380,7 @@ func (h *FeedbackHandler) DeleteComment(w http.ResponseWriter, r *http.Request) if err := h.service.DeleteComment(r.Context(), uint(commentID)); err != nil { logger.Get().Error("Failed to delete comment", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + http.Error(w, err.Error(), http.StatusBadRequest) return } diff --git a/main_dc/yalarba/api_yal/internal/domain/feetback/service.go b/main_dc/yalarba/api_yal/internal/domain/feetback/service.go index 9adfe49..82ea5fb 100644 --- a/main_dc/yalarba/api_yal/internal/domain/feetback/service.go +++ b/main_dc/yalarba/api_yal/internal/domain/feetback/service.go @@ -579,4 +579,171 @@ func (s *feedbackServiceImpl) isValidPlatform(platform models.PlatformType) bool } } return false +} + +// Добавьте в конец файла service.go: + +// AddComment добавляет комментарий к отзыву +func (s *feedbackServiceImpl) AddComment(ctx context.Context, feedbackID uint, text string) (*models.Comment, error) { + if feedbackID == 0 { + return nil, errors.New("invalid feedback ID") + } + + if strings.TrimSpace(text) == "" { + return nil, errors.New("comment text cannot be empty") + } + + if len(text) > 1000 { + return nil, errors.New("comment text cannot exceed 1000 characters") + } + + // Проверяем существование отзыва + _, err := s.feedbackRepository.GetByID(feedbackID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("feedback with ID %d not found", feedbackID) + } + return nil, fmt.Errorf("failed to get feedback: %w", err) + } + + // Получаем userID из контекста + userID, ok := ctx.Value(middleware.UserIDKey).(uint) + if !ok { + return nil, errors.New("unauthorized") + } + + comment := &models.Comment{ + FeedbackID: feedbackID, + UserID: userID, + Text: text, + } + + if err := s.feedbackRepository.CreateComment(comment); err != nil { + return nil, fmt.Errorf("failed to create comment: %w", err) + } + + // Обновляем счетчик комментариев + if err := s.updateCommentCount(feedbackID); err != nil { + // Логируем ошибку, но не возвращаем, так как комментарий уже создан + _ = err + } + + return comment, nil +} + +// GetComments возвращает комментарии к отзыву +func (s *feedbackServiceImpl) GetComments(ctx context.Context, feedbackID uint, page, pageSize int) ([]models.Comment, int64, error) { + if feedbackID == 0 { + return nil, 0, errors.New("invalid feedback ID") + } + + // Проверяем существование отзыва + _, err := s.feedbackRepository.GetByID(feedbackID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, fmt.Errorf("feedback with ID %d not found", feedbackID) + } + return nil, 0, fmt.Errorf("failed to get feedback: %w", err) + } + + offset, limit := s.normalizePagination(page, pageSize) + + comments, err := s.feedbackRepository.GetComments(feedbackID, offset, limit) + if err != nil { + return nil, 0, fmt.Errorf("failed to get comments: %w", err) + } + + total, err := s.feedbackRepository.GetCommentCount(feedbackID) + if err != nil { + return nil, 0, fmt.Errorf("failed to count comments: %w", err) + } + + return comments, int64(total), nil +} + +// UpdateComment обновляет комментарий +func (s *feedbackServiceImpl) UpdateComment(ctx context.Context, commentID uint, text string) error { + if commentID == 0 { + return errors.New("invalid comment ID") + } + + if strings.TrimSpace(text) == "" { + return errors.New("comment text cannot be empty") + } + + if len(text) > 1000 { + return errors.New("comment text cannot exceed 1000 characters") + } + + comment, err := s.feedbackRepository.GetCommentByID(commentID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("comment with ID %d not found", commentID) + } + return fmt.Errorf("failed to get comment: %w", err) + } + + // Проверяем, что текущий пользователь является владельцем + userID, ok := ctx.Value(middleware.UserIDKey).(uint) + if !ok { + return errors.New("unauthorized") + } + + if comment.UserID != userID { + return errors.New("unauthorized: cannot update comment owned by another user") + } + + comment.Text = text + if err := s.feedbackRepository.UpdateComment(comment); err != nil { + return fmt.Errorf("failed to update comment: %w", err) + } + + return nil +} + +// DeleteComment удаляет комментарий +func (s *feedbackServiceImpl) DeleteComment(ctx context.Context, commentID uint) error { + if commentID == 0 { + return errors.New("invalid comment ID") + } + + comment, err := s.feedbackRepository.GetCommentByID(commentID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("comment with ID %d not found", commentID) + } + return fmt.Errorf("failed to get comment: %w", err) + } + + // Проверяем, что текущий пользователь является владельцем + userID, ok := ctx.Value(middleware.UserIDKey).(uint) + if !ok { + return errors.New("unauthorized") + } + + if comment.UserID != userID { + return errors.New("unauthorized: cannot delete comment owned by another user") + } + + if err := s.feedbackRepository.DeleteComment(commentID); err != nil { + return fmt.Errorf("failed to delete comment: %w", err) + } + + // Обновляем счетчик комментариев + if err := s.updateCommentCount(comment.FeedbackID); err != nil { + // Логируем ошибку, но не возвращаем + _ = err + } + + return nil +} + +// updateCommentCount обновляет счетчик комментариев у отзыва +func (s *feedbackServiceImpl) updateCommentCount(feedbackID uint) error { + count, err := s.feedbackRepository.GetCommentCount(feedbackID) + if err != nil { + return err + } + + return s.feedbackRepository.UpdateCommentCount(feedbackID, count) } \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/repository/feedback_repository.go b/main_dc/yalarba/api_yal/internal/repository/feedback_repository.go index 16ce787..5d94dfb 100644 --- a/main_dc/yalarba/api_yal/internal/repository/feedback_repository.go +++ b/main_dc/yalarba/api_yal/internal/repository/feedback_repository.go @@ -5,6 +5,7 @@ import ( ) // FeedbackRepository интерфейс для операций с моделью Feedback +// //go:generate mockgen -destination=mocks/feedback_repository.go -package=mocks . FeedbackRepository type FeedbackRepository interface { // Create создает новый отзыв @@ -51,4 +52,26 @@ type FeedbackRepository interface { // Search находит отзывы по тексту Search(query string, offset, limit int) ([]models.Feedback, error) -} \ No newline at end of file + + // Добавьте в интерфейс FeedbackRepository: + + // Comment methods + CreateComment(comment *models.Comment) error + GetCommentByID(id uint) (*models.Comment, error) + UpdateComment(comment *models.Comment) error + DeleteComment(id uint) error + + // Additional methods for stats + GetAverageScore() (float64, error) + GetPlatformStats() (map[models.PlatformType]int, error) + GetScoreDistribution() (map[int]int, error) + + // Count methods + CountByOwner(ownerID uint) (int64, error) + CountByObject(objectID uint) (int64, error) + CountByPlatform(platform models.PlatformType) (int64, error) + CountBySearch(query string) (int64, error) + + // GetObject by ID (general method) + GetObjectByID(id uint) (*models.Object, error) +} diff --git a/main_dc/yalarba/api_yal/internal/repository/feedback_repository_impl.go b/main_dc/yalarba/api_yal/internal/repository/feedback_repository_impl.go index df203aa..c4e3675 100644 --- a/main_dc/yalarba/api_yal/internal/repository/feedback_repository_impl.go +++ b/main_dc/yalarba/api_yal/internal/repository/feedback_repository_impl.go @@ -156,4 +156,128 @@ func (r *feedbackRepositoryImpl) getObjectByID(id uint) (*models.Object, error) return nil, err } return &object, nil -} \ No newline at end of file +} + +// CreateComment создает новый комментарий +func (r *feedbackRepositoryImpl) CreateComment(comment *models.Comment) error { + return r.db.Create(comment).Error +} + +// GetCommentByID возвращает комментарий по ID +func (r *feedbackRepositoryImpl) GetCommentByID(id uint) (*models.Comment, error) { + var comment models.Comment + err := r.db.First(&comment, id).Error + if err != nil { + return nil, err + } + return &comment, nil +} + +// UpdateComment обновляет комментарий +func (r *feedbackRepositoryImpl) UpdateComment(comment *models.Comment) error { + return r.db.Save(comment).Error +} + +// DeleteComment удаляет комментарий +func (r *feedbackRepositoryImpl) DeleteComment(id uint) error { + return r.db.Delete(&models.Comment{}, id).Error +} + +// GetAverageScore возвращает средний балл отзывов +func (r *feedbackRepositoryImpl) GetAverageScore() (float64, error) { + var avg float64 + err := r.db.Model(&models.Feedback{}).Select("COALESCE(AVG(score), 0)").Scan(&avg).Error + return avg, err +} + +// GetPlatformStats возвращает статистику по платформам +func (r *feedbackRepositoryImpl) GetPlatformStats() (map[models.PlatformType]int, error) { + type Result struct { + Platform models.PlatformType + Count int + } + + var results []Result + err := r.db.Model(&models.Feedback{}). + Select("platform, COUNT(*) as count"). + Group("platform"). + Scan(&results).Error + + if err != nil { + return nil, err + } + + stats := make(map[models.PlatformType]int) + for _, r := range results { + stats[r.Platform] = r.Count + } + + return stats, nil +} + +// GetScoreDistribution возвращает распределение оценок +func (r *feedbackRepositoryImpl) GetScoreDistribution() (map[int]int, error) { + type Result struct { + Score int + Count int + } + + var results []Result + err := r.db.Model(&models.Feedback{}). + Select("score, COUNT(*) as count"). + Group("score"). + Scan(&results).Error + + if err != nil { + return nil, err + } + + distribution := make(map[int]int) + for i := 1; i <= 5; i++ { + distribution[i] = 0 + } + + for _, r := range results { + distribution[r.Score] = r.Count + } + + return distribution, nil +} + +// CountByOwner возвращает количество отзывов владельца +func (r *feedbackRepositoryImpl) CountByOwner(ownerID uint) (int64, error) { + var count int64 + err := r.db.Model(&models.Feedback{}).Where("owner_id = ?", ownerID).Count(&count).Error + return count, err +} + +// CountByObject возвращает количество отзывов объекта +func (r *feedbackRepositoryImpl) CountByObject(objectID uint) (int64, error) { + var count int64 + err := r.db.Model(&models.Feedback{}).Where("object_id = ?", objectID).Count(&count).Error + return count, err +} + +// CountByPlatform возвращает количество отзывов платформы +func (r *feedbackRepositoryImpl) CountByPlatform(platform models.PlatformType) (int64, error) { + var count int64 + err := r.db.Model(&models.Feedback{}).Where("platform = ?", platform).Count(&count).Error + return count, err +} + +// CountBySearch возвращает количество отзывов по поисковому запросу +func (r *feedbackRepositoryImpl) CountBySearch(query string) (int64, error) { + var count int64 + err := r.db.Model(&models.Feedback{}).Where("text LIKE ?", "%"+query+"%").Count(&count).Error + return count, err +} + +// GetObjectByID возвращает объект по ID +func (r *feedbackRepositoryImpl) GetObjectByID(id uint) (*models.Object, error) { + var object models.Object + err := r.db.First(&object, id).Error + if err != nil { + return nil, err + } + return &object, nil +}