package feetback import ( "api_yal/internal/models" "api_yal/internal/logger" "encoding/json" "net/http" "strconv" "github.com/go-chi/chi/v5" "go.uber.org/zap" ) // FeedbackHandler обработчик для отзывов type FeedbackHandler struct { service FeedbackService log *zap.Logger } // NewFeedbackHandler создает новый обработчик func NewFeedbackHandler(service FeedbackService) *FeedbackHandler { return &FeedbackHandler{ service: service, log: logger.Get(), } } // getUserIDFromContext получает ID пользователя из контекста func (h *FeedbackHandler) getUserIDFromContext(r *http.Request) (uint, bool) { userID, ok := r.Context().Value("user_id").(uint) return userID, ok } // isAdminFromContext проверяет, является ли пользователь админом func (h *FeedbackHandler) isAdminFromContext(r *http.Request) bool { isAdmin, ok := r.Context().Value("is_admin").(bool) return ok && isAdmin } // parsePagination парсит параметры пагинации func (h *FeedbackHandler) parsePagination(r *http.Request) (offset, limit int) { offset, _ = strconv.Atoi(r.URL.Query().Get("offset")) limit, _ = strconv.Atoi(r.URL.Query().Get("limit")) if limit <= 0 || limit > 100 { limit = 20 } if offset < 0 { offset = 0 } return } // sendJSON отправляет JSON ответ func (h *FeedbackHandler) sendJSON(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(data); err != nil { h.log.Error("Failed to encode JSON response", zap.Error(err)) } } // sendError отправляет ошибку func (h *FeedbackHandler) sendError(w http.ResponseWriter, status int, message string) { h.sendJSON(w, status, ErrorResponse{ Error: http.StatusText(status), Message: message, Code: status, }) } // CreateFeedback создает новый отзыв // POST /feedbacks func (h *FeedbackHandler) CreateFeedback(w http.ResponseWriter, r *http.Request) { userID, ok := h.getUserIDFromContext(r) if !ok { h.sendError(w, http.StatusUnauthorized, "unauthorized") return } var req CreateFeedbackRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.sendError(w, http.StatusBadRequest, "invalid request body") return } feedback := &models.Feedback{ ObjectID: req.ObjectID, Score: req.Rating, Text: req.Text, Platform: req.Platform, } if err := h.service.Create(feedback, userID); err != nil { h.log.Error("Failed to create feedback", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to create feedback") return } // Загружаем созданный отзыв с данными владельца и объекта created, _ := h.service.GetByID(feedback.ID) response := h.mapFeedbackToResponse(created) h.sendJSON(w, http.StatusCreated, response) } // GetFeedbackByID получает отзыв по ID // GET /feedbacks/{id} func (h *FeedbackHandler) GetFeedbackByID(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { h.sendError(w, http.StatusBadRequest, "invalid feedback id") return } feedback, err := h.service.GetByID(uint(id)) if err != nil { h.sendError(w, http.StatusNotFound, "feedback not found") return } response := h.mapFeedbackToResponse(feedback) h.sendJSON(w, http.StatusOK, response) } // UpdateFeedback обновляет отзыв // PUT /feedbacks/{id} func (h *FeedbackHandler) UpdateFeedback(w http.ResponseWriter, r *http.Request) { userID, ok := h.getUserIDFromContext(r) if !ok { h.sendError(w, http.StatusUnauthorized, "unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { h.sendError(w, http.StatusBadRequest, "invalid feedback id") return } var req UpdateFeedbackRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.sendError(w, http.StatusBadRequest, "invalid request body") return } updates := make(map[string]interface{}) if req.Rating != nil { updates["rating"] = *req.Rating } if req.Text != nil { updates["text"] = *req.Text } if req.Platform != nil { updates["platform"] = *req.Platform } if req.MediaURLs != nil { updates["media_urls"] = req.MediaURLs } if err := h.service.Update(uint(id), userID, updates); err != nil { if err.Error() == "you can only update your own feedback" { h.sendError(w, http.StatusForbidden, err.Error()) return } h.log.Error("Failed to update feedback", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to update feedback") return } feedback, _ := h.service.GetByID(uint(id)) response := h.mapFeedbackToResponse(feedback) h.sendJSON(w, http.StatusOK, response) } // DeleteFeedback удаляет отзыв // DELETE /feedbacks/{id} func (h *FeedbackHandler) DeleteFeedback(w http.ResponseWriter, r *http.Request) { userID, ok := h.getUserIDFromContext(r) if !ok { h.sendError(w, http.StatusUnauthorized, "unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { h.sendError(w, http.StatusBadRequest, "invalid feedback id") return } isAdmin := h.isAdminFromContext(r) if err := h.service.Delete(uint(id), userID, isAdmin); err != nil { if err.Error() == "you can only delete your own feedback" { h.sendError(w, http.StatusForbidden, err.Error()) return } h.log.Error("Failed to delete feedback", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to delete feedback") return } h.sendJSON(w, http.StatusNoContent, nil) } // ListFeedbacks возвращает список отзывов с пагинацией // GET /feedbacks func (h *FeedbackHandler) ListFeedbacks(w http.ResponseWriter, r *http.Request) { offset, limit := h.parsePagination(r) feedbacks, total, err := h.service.List(offset, limit) if err != nil { h.log.Error("Failed to list feedbacks", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks") return } response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit) h.sendJSON(w, http.StatusOK, response) } // GetMyFeedbacks возвращает отзывы текущего пользователя // GET /feedbacks/my func (h *FeedbackHandler) GetMyFeedbacks(w http.ResponseWriter, r *http.Request) { userID, ok := h.getUserIDFromContext(r) if !ok { h.sendError(w, http.StatusUnauthorized, "unauthorized") return } offset, limit := h.parsePagination(r) feedbacks, total, err := h.service.ListByOwner(userID, offset, limit) if err != nil { h.log.Error("Failed to list user feedbacks", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks") return } response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit) h.sendJSON(w, http.StatusOK, response) } // GetFeedbacksByObject возвращает отзывы по объекту // GET /feedbacks/object/{objectID} func (h *FeedbackHandler) GetFeedbacksByObject(w http.ResponseWriter, r *http.Request) { objectIDStr := chi.URLParam(r, "objectID") objectID, err := strconv.ParseUint(objectIDStr, 10, 32) if err != nil { h.sendError(w, http.StatusBadRequest, "invalid object id") return } offset, limit := h.parsePagination(r) feedbacks, total, err := h.service.ListByObject(uint(objectID), offset, limit) if err != nil { h.log.Error("Failed to list feedbacks by object", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks") return } response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit) h.sendJSON(w, http.StatusOK, response) } // GetFeedbacksByPlatform возвращает отзывы по платформе // GET /feedbacks/platform/{platform} func (h *FeedbackHandler) GetFeedbacksByPlatform(w http.ResponseWriter, r *http.Request) { platformStr := chi.URLParam(r, "platform") platform := models.PlatformType(platformStr) offset, limit := h.parsePagination(r) feedbacks, total, err := h.service.ListByPlatform(platform, offset, limit) if err != nil { h.log.Error("Failed to list feedbacks by platform", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks") return } response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit) h.sendJSON(w, http.StatusOK, response) } // SearchFeedbacks ищет отзывы по тексту // GET /feedbacks/search func (h *FeedbackHandler) SearchFeedbacks(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") if query == "" { h.sendError(w, http.StatusBadRequest, "search query is required") return } offset, limit := h.parsePagination(r) feedbacks, total, err := h.service.Search(query, offset, limit) if err != nil { h.log.Error("Failed to search feedbacks", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to search feedbacks") return } response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit) h.sendJSON(w, http.StatusOK, response) } // GetFeedbackComments возвращает комментарии к отзыву // GET /feedbacks/{id}/comments func (h *FeedbackHandler) GetFeedbackComments(w http.ResponseWriter, r *http.Request) { idStr := chi.URLParam(r, "id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { h.sendError(w, http.StatusBadRequest, "invalid feedback id") return } offset, limit := h.parsePagination(r) comments, total, err := h.service.GetComments(uint(id), offset, limit) if err != nil { h.log.Error("Failed to get comments", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to get comments") return } response := h.mapCommentListToResponse(comments, total, offset, limit) h.sendJSON(w, http.StatusOK, response) } // AddComment добавляет комментарий к отзыву // POST /feedbacks/{id}/comments func (h *FeedbackHandler) AddComment(w http.ResponseWriter, r *http.Request) { userID, ok := h.getUserIDFromContext(r) if !ok { h.sendError(w, http.StatusUnauthorized, "unauthorized") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { h.sendError(w, http.StatusBadRequest, "invalid feedback id") return } var req CreateCommentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.sendError(w, http.StatusBadRequest, "invalid request body") return } comment, err := h.service.AddComment(uint(id), userID, req.Text) if err != nil { if err.Error() == "feedback not found" { h.sendError(w, http.StatusNotFound, err.Error()) return } h.log.Error("Failed to add comment", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to add comment") return } response := h.mapCommentToResponse(comment) h.sendJSON(w, http.StatusCreated, response) } // UpdateComment обновляет комментарий // PUT /feedbacks/{id}/comments/{commentID} func (h *FeedbackHandler) UpdateComment(w http.ResponseWriter, r *http.Request) { userID, ok := h.getUserIDFromContext(r) if !ok { h.sendError(w, http.StatusUnauthorized, "unauthorized") return } commentIDStr := chi.URLParam(r, "commentID") commentID, err := strconv.ParseUint(commentIDStr, 10, 32) if err != nil { h.sendError(w, http.StatusBadRequest, "invalid comment id") return } var req UpdateCommentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.sendError(w, http.StatusBadRequest, "invalid request body") return } isAdmin := h.isAdminFromContext(r) if err := h.service.UpdateComment(uint(commentID), userID, req.Text, isAdmin); err != nil { if err.Error() == "you can only update your own comments" { h.sendError(w, http.StatusForbidden, err.Error()) return } h.log.Error("Failed to update comment", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to update comment") return } h.sendJSON(w, http.StatusOK, map[string]string{"message": "comment updated successfully"}) } // DeleteComment удаляет комментарий // DELETE /feedbacks/{id}/comments/{commentID} func (h *FeedbackHandler) DeleteComment(w http.ResponseWriter, r *http.Request) { userID, ok := h.getUserIDFromContext(r) if !ok { h.sendError(w, http.StatusUnauthorized, "unauthorized") return } commentIDStr := chi.URLParam(r, "commentID") commentID, err := strconv.ParseUint(commentIDStr, 10, 32) if err != nil { h.sendError(w, http.StatusBadRequest, "invalid comment id") return } isAdmin := h.isAdminFromContext(r) if err := h.service.DeleteComment(uint(commentID), userID, isAdmin); err != nil { if err.Error() == "you can only delete your own comments" { h.sendError(w, http.StatusForbidden, err.Error()) return } h.log.Error("Failed to delete comment", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to delete comment") return } h.sendJSON(w, http.StatusNoContent, nil) } // GetFeedbackStats возвращает статистику по отзывам // GET /feedbacks/stats func (h *FeedbackHandler) GetFeedbackStats(w http.ResponseWriter, r *http.Request) { stats, err := h.service.GetStats() if err != nil { h.log.Error("Failed to get stats", zap.Error(err)) h.sendError(w, http.StatusInternalServerError, "failed to get stats") return } h.sendJSON(w, http.StatusOK, stats) } // mapFeedbackToResponse маппинг модели в DTO ответа func (h *FeedbackHandler) mapFeedbackToResponse(f *models.Feedback) FeedbackResponse { resp := FeedbackResponse{ ID: f.ID, OwnerID: f.OwnerID, ObjectID: f.ObjectID, Rating: f.Score, Text: f.Text, Platform: f.Platform, CommentCount: f.CommentCount, CreatedAt: f.CreatedAt, UpdatedAt: f.UpdatedAt, } if f.OwnerID != 0 { resp.Owner = &AccountBriefResponse{ ID: f.Owner.ID, Username: f.Owner.FullName, } } if f.ObjectID != 0 { resp.Object = &ObjectBriefResponse{ ID: f.Object.ID, Name: f.Object.Owner.FullName, Description: f.Object.ShortDescription, } } return resp } // mapFeedbackListToResponse маппинг списка отзывов в DTO ответа func (h *FeedbackHandler) mapFeedbackListToResponse(feedbacks []models.Feedback, total int64, offset, limit int) FeedbackListResponse { data := make([]FeedbackResponse, len(feedbacks)) for i, f := range feedbacks { data[i] = h.mapFeedbackToResponse(&f) } resp := FeedbackListResponse{ Data: data, Total: total, Offset: offset, Limit: limit, } nextOffset := offset + limit if int64(nextOffset) < total { resp.NextOffset = &nextOffset } return resp } // mapCommentToResponse маппинг комментария в DTO ответа func (h *FeedbackHandler) mapCommentToResponse(c *models.Comment) CommentResponse { resp := CommentResponse{ ID: c.ID, FeedbackID: c.FeedbackID, AccountID: c.AuthorID, Text: c.Text, CreatedAt: c.CreatedAt, UpdatedAt: c.UpdatedAt, } if c.AuthorID != 0 { resp.Account = &AccountBriefResponse{ ID: c.Author.ID, Username: c.Author.FullName, } } return resp } // mapCommentListToResponse маппинг списка комментариев в DTO ответа func (h *FeedbackHandler) mapCommentListToResponse(comments []models.Comment, total, offset, limit int) CommentListResponse { data := make([]CommentResponse, len(comments)) for i, c := range comments { data[i] = h.mapCommentToResponse(&c) } resp := CommentListResponse{ Data: data, Total: total, Offset: offset, Limit: limit, } nextOffset := offset + limit if nextOffset < total { resp.NextOffset = &nextOffset } return resp }