diff --git a/main_dc/yalarba/api_yal/internal/domain/appeal/router.go b/main_dc/yalarba/api_yal/internal/domain/appeal/router.go index 81d61cf..8b50161 100644 --- a/main_dc/yalarba/api_yal/internal/domain/appeal/router.go +++ b/main_dc/yalarba/api_yal/internal/domain/appeal/router.go @@ -1,9 +1,3 @@ package appeal import () - -func NewHandler(service AppealService) *AppealHandler { - return &AppealHandler{ - service: service, - } -} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/feetback/dto.go b/main_dc/yalarba/api_yal/internal/domain/feetback/dto.go index 3d6b07f..5ecf11a 100644 --- a/main_dc/yalarba/api_yal/internal/domain/feetback/dto.go +++ b/main_dc/yalarba/api_yal/internal/domain/feetback/dto.go @@ -1,88 +1,107 @@ package feetback import ( - "api_yal/internal/domain/account" - "api_yal/internal/domain/comment" - "api_yal/internal/domain/object" "api_yal/internal/models" "time" ) -// CreateFeedbackRequest - DTO для создания отзыва -// Обязательные поля: ObjectID, Platform, Score, Text -// Score должен быть от 1 до 5 -// Platform должен быть одним из допустимых значений (entrepreneur, tourist) +// CreateFeedbackRequest DTO для создания отзыва type CreateFeedbackRequest struct { - ObjectID uint `json:"object_id" binding:"required"` - Platform models.PlatformType `json:"platform" binding:"required,oneof=entrepreneur tourist"` - Score int `json:"score" binding:"required,min=1,max=5"` - Text string `json:"text" binding:"required"` + ObjectID uint `json:"object_id" binding:"required"` + Rating int `json:"rating" binding:"required,min=1,max=5"` + Text string `json:"text" binding:"required,min=1,max=2000"` + Platform models.PlatformType `json:"platform" binding:"required"` + MediaURLs []string `json:"media_urls"` } -// UpdateFeedbackRequest - DTO для обновления отзыва -// Все поля опциональны, позволяют обновлять только указанные данные +// UpdateFeedbackRequest DTO для обновления отзыва type UpdateFeedbackRequest struct { - Score *int `json:"score" binding:"omitempty,min=1,max=5"` - Text *string `json:"text"` + Rating *int `json:"rating,omitempty" binding:"omitempty,min=1,max=5"` + Text *string `json:"text,omitempty" binding:"omitempty,min=1,max=2000"` + Platform *models.PlatformType `json:"platform,omitempty"` + MediaURLs []string `json:"media_urls"` } -// FeedbackResponse - DTO для полного ответа с отзывом -// Включает всю информацию о отзыве, включая связанные данные -// Owner и Object могут быть nil, если предзагрузка не была выполнена +// FeedbackResponse DTO для ответа type FeedbackResponse struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt *time.Time `json:"deleted_at,omitempty"` - OwnerID uint `json:"owner_id"` - Owner *account.AccountResponse `json:"owner,omitempty"` - ObjectID uint `json:"object_id"` - Object *object.ObjectShortResponse `json:"object,omitempty"` - Platform models.PlatformType `json:"platform"` - Score int `json:"score"` - Text string `json:"text"` - CommentCount int `json:"comment_count"` - Comments []comment.CommentShortResponse `json:"comments,omitempty"` + ID uint `json:"id"` + OwnerID uint `json:"owner_id"` + Owner *AccountBriefResponse `json:"owner,omitempty"` + ObjectID uint `json:"object_id"` + Object *ObjectBriefResponse `json:"object,omitempty"` + Rating int `json:"rating"` + Text string `json:"text"` + Platform models.PlatformType `json:"platform"` + MediaURLs []string `json:"media_urls"` + CommentCount int `json:"comment_count"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } -// FeedbackShortResponse - DTO для краткого ответа (вложенный в объект) -// Используется при возврате отзывов в составе других объектов -// OwnerName берется из Account.OwnerName или формируется из имени пользователя -type FeedbackShortResponse struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"created_at"` - OwnerID uint `json:"owner_id"` - OwnerName string `json:"owner_name,omitempty"` - Platform models.PlatformType `json:"platform"` - Score int `json:"score"` - Text string `json:"text"` +// AccountBriefResponse краткая информация о пользователе +type AccountBriefResponse struct { + ID uint `json:"id"` + Username string `json:"username"` + AvatarURL string `json:"avatar_url"` } -// FeedbackListResponse - DTO для списка отзывов с пагинацией -// Структура для возврата списка отзывов с метаданными пагинации +// ObjectBriefResponse краткая информация об объекте +type ObjectBriefResponse struct { + ID uint `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} + +// FeedbackListResponse ответ со списком отзывов type FeedbackListResponse struct { - Items []FeedbackShortResponse `json:"items"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` - TotalPages int `json:"total_pages"` + Data []FeedbackResponse `json:"data"` + Total int64 `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + NextOffset *int `json:"next_offset,omitempty"` } -// SearchFeedbacksRequest - DTO для поиска отзывов -// Используется для параметров поиска -// Запрос должен быть не пустым -type SearchFeedbacksRequest struct { - Query string `json:"q" binding:"required"` - Page int `json:"page"` - PageSize int `json:"page_size"` +// CreateCommentRequest DTO для создания комментария +type CreateCommentRequest struct { + Text string `json:"text" binding:"required,min=1,max=1000"` } -// FeedbackStatsResponse - DTO для статистики по отзывам -// Используется для агрегированной информации -// AverageScore может быть 0, если нет отзывов +// UpdateCommentRequest DTO для обновления комментария +type UpdateCommentRequest struct { + Text string `json:"text" binding:"required,min=1,max=1000"` +} + +// CommentResponse DTO для ответа с комментарием +type CommentResponse struct { + ID uint `json:"id"` + FeedbackID uint `json:"feedback_id"` + AccountID uint `json:"account_id"` + Account *AccountBriefResponse `json:"account,omitempty"` + Text string `json:"text"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// CommentListResponse ответ со списком комментариев +type CommentListResponse struct { + Data []CommentResponse `json:"data"` + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + NextOffset *int `json:"next_offset,omitempty"` +} + +// FeedbackStatsResponse статистика по отзывам type FeedbackStatsResponse struct { - TotalCount int `json:"total_count"` - AverageScore float64 `json:"average_score"` - PlatformStats map[models.PlatformType]int `json:"platform_stats"` - ScoreDistribution map[int]int `json:"score_distribution"` // распределение по баллам (1-5) + TotalFeedbacks int64 `json:"total_feedbacks"` + AverageRating float64 `json:"average_rating"` + RatingDistribution map[int]int64 `json:"rating_distribution"` + PlatformStats map[models.PlatformType]int64 `json:"platform_stats"` } + +// ErrorResponse ответ с ошибкой +type ErrorResponse struct { + Error string `json:"error"` + Message string `json:"message"` + Code int `json:"code"` +} \ No newline at end of file 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 45677d0..0d7e23b 100644 --- a/main_dc/yalarba/api_yal/internal/domain/feetback/handler.go +++ b/main_dc/yalarba/api_yal/internal/domain/feetback/handler.go @@ -1,388 +1,547 @@ package feetback import ( + "api_yal/internal/models" + "api_yal/internal/logger" "encoding/json" "net/http" "strconv" - "api_yal/internal/logger" - "api_yal/internal/middleware" - "api_yal/internal/models" - "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 { - http.Error(w, "Invalid request body", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid request body") return } - 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.StatusBadRequest) + 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 } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(response) + // Загружаем созданный отзыв с данными владельца и объекта + created, _ := h.service.GetByID(feedback.ID) + response := h.mapFeedbackToResponse(created) + + h.sendJSON(w, http.StatusCreated, response) } -// GetFeedbackByID возвращает отзыв по ID +// 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 { - http.Error(w, "Invalid ID", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid feedback id") return } - response, err := h.service.GetByID(r.Context(), uint(id)) + feedback, err := h.service.GetByID(uint(id)) if err != nil { - logger.Get().Error("Failed to get feedback", zap.Error(err)) - http.Error(w, err.Error(), http.StatusNotFound) + h.sendError(w, http.StatusNotFound, "feedback not found") return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + response := h.mapFeedbackToResponse(feedback) + h.sendJSON(w, http.StatusOK, response) } -// UpdateFeedback обновляет существующий отзыв +// 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 { - http.Error(w, "Invalid ID", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid feedback id") return } var req UpdateFeedbackRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid request body") return } - 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.StatusBadRequest) + 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 } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + 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 { - http.Error(w, "Invalid ID", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid feedback id") return } - 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.StatusBadRequest) + 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 } - w.WriteHeader(http.StatusNoContent) + h.sendJSON(w, http.StatusNoContent, nil) } // ListFeedbacks возвращает список отзывов с пагинацией +// GET /feedbacks func (h *FeedbackHandler) ListFeedbacks(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 - } + offset, limit := h.parsePagination(r) - response, err := h.service.List(r.Context(), page, pageSize) + feedbacks, total, err := h.service.List(offset, limit) if err != nil { - logger.Get().Error("Failed to list feedbacks", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + h.log.Error("Failed to list feedbacks", zap.Error(err)) + h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks") return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + 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 := middleware.GetUserID(r.Context()) + userID, ok := h.getUserIDFromContext(r) if !ok { - http.Error(w, "Unauthorized", http.StatusUnauthorized) + h.sendError(w, http.StatusUnauthorized, "unauthorized") return } - - 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 - } - response, err := h.service.ListByOwner(r.Context(), userID, page, pageSize) + offset, limit := h.parsePagination(r) + + feedbacks, total, err := h.service.ListByOwner(userID, offset, limit) if err != nil { - logger.Get().Error("Failed to get user feedbacks", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + h.log.Error("Failed to list user feedbacks", zap.Error(err)) + h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks") return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + 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 { - http.Error(w, "Invalid object ID", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid object id") return } - 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 - } + offset, limit := h.parsePagination(r) - response, err := h.service.ListByObject(r.Context(), uint(objectID), page, pageSize) + feedbacks, total, err := h.service.ListByObject(uint(objectID), offset, limit) if err != nil { - logger.Get().Error("Failed to get object feedbacks", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + h.log.Error("Failed to list feedbacks by object", zap.Error(err)) + h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks") return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + 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) - 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 - } + offset, limit := h.parsePagination(r) - response, err := h.service.ListByPlatform(r.Context(), platform, page, pageSize) + feedbacks, total, err := h.service.ListByPlatform(platform, offset, limit) if err != nil { - logger.Get().Error("Failed to get platform feedbacks", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + h.log.Error("Failed to list feedbacks by platform", zap.Error(err)) + h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks") return } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) + 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 == "" { - http.Error(w, "Search query is required", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "search query is required") return } - 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 - } + offset, limit := h.parsePagination(r) - response, err := h.service.Search(r.Context(), query, page, pageSize) + feedbacks, total, err := h.service.Search(query, offset, limit) if err != nil { - logger.Get().Error("Failed to search feedbacks", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + h.log.Error("Failed to search feedbacks", zap.Error(err)) + h.sendError(w, http.StatusInternalServerError, "failed to search feedbacks") return } - w.Header().Set("Content-Type", "application/json") - 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) + 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 { - http.Error(w, "Invalid ID", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid feedback id") return } - 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 - } + offset, limit := h.parsePagination(r) - comments, total, err := h.service.GetComments(r.Context(), uint(id), page, pageSize) + comments, total, err := h.service.GetComments(uint(id), offset, limit) if err != nil { - logger.Get().Error("Failed to get comments", zap.Error(err)) - http.Error(w, err.Error(), http.StatusInternalServerError) + h.log.Error("Failed to get comments", zap.Error(err)) + h.sendError(w, http.StatusInternalServerError, "failed to get comments") 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(response) + 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 { - http.Error(w, "Invalid ID", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid feedback id") return } - var req struct { - Text string `json:"text" binding:"required"` - } + var req CreateCommentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid request body") return } - comment, err := h.service.AddComment(r.Context(), uint(id), req.Text) + comment, err := h.service.AddComment(uint(id), userID, req.Text) if err != nil { - logger.Get().Error("Failed to add comment", zap.Error(err)) - http.Error(w, err.Error(), http.StatusBadRequest) + 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 } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusCreated) - json.NewEncoder(w).Encode(comment) + 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 { - http.Error(w, "Invalid comment ID", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid comment id") return } - var req struct { - Text string `json:"text" binding:"required"` - } + var req UpdateCommentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid request body") return } - 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.StatusBadRequest) + 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 } - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{"message": "Comment updated successfully"}) + 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 { - http.Error(w, "Invalid comment ID", http.StatusBadRequest) + h.sendError(w, http.StatusBadRequest, "invalid comment id") return } - 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.StatusBadRequest) + 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 } - w.WriteHeader(http.StatusNoContent) + 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 } \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/feetback/router.go b/main_dc/yalarba/api_yal/internal/domain/feetback/router.go index 31d858e..1d3c0c6 100644 --- a/main_dc/yalarba/api_yal/internal/domain/feetback/router.go +++ b/main_dc/yalarba/api_yal/internal/domain/feetback/router.go @@ -14,39 +14,33 @@ func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { l.Info("Registering routes for feetback") feedbackRepo := repository.NewFeedbackRepository(db) - feedbackService := NewFeedbackServiceImpl(feedbackRepo) + feedbackService := NewFeedbackServiceImpl(feedbackRepo, db) feedbackHandler := NewFeedbackHandler(feedbackService) // Группируем маршруты для отзывов r.Route("/feedbacks", func(r chi.Router) { // Публичные маршруты (не требуют аутентификации) - r.Get("/", feedbackHandler.ListFeedbacks) // GET /api/v1/feedbacks - r.Get("/search", feedbackHandler.SearchFeedbacks) // GET /api/v1/feedbacks/search?q=query - r.Get("/{id}", feedbackHandler.GetFeedbackByID) // GET /api/v1/feedbacks/{id} - - // Маршруты для фильтрации - r.Get("/object/{objectID}", feedbackHandler.GetFeedbacksByObject) // GET /api/v1/feedbacks/object/{objectID} - r.Get("/platform/{platform}", feedbackHandler.GetFeedbacksByPlatform) // GET /api/v1/feedbacks/platform/{platform} - - // Маршруты для комментариев (публичные) - r.Get("/{id}/comments", feedbackHandler.GetFeedbackComments) // GET /api/v1/feedbacks/{id}/comments + r.Get("/", feedbackHandler.ListFeedbacks) + r.Get("/search", feedbackHandler.SearchFeedbacks) + r.Get("/stats", feedbackHandler.GetFeedbackStats) // Новый эндпоинт + r.Get("/{id}", feedbackHandler.GetFeedbackByID) + r.Get("/object/{objectID}", feedbackHandler.GetFeedbacksByObject) + r.Get("/platform/{platform}", feedbackHandler.GetFeedbacksByPlatform) + r.Get("/{id}/comments", feedbackHandler.GetFeedbackComments) // Защищенные маршруты (требуют аутентификации) r.Group(func(r chi.Router) { - // Здесь можно добавить middleware для проверки JWT токена r.Use(middleware.AuthMiddleware(jwtSecret)) - r.Post("/", feedbackHandler.CreateFeedback) // POST /api/v1/feedbacks - r.Put("/{id}", feedbackHandler.UpdateFeedback) // PUT /api/v1/feedbacks/{id} - r.Delete("/{id}", feedbackHandler.DeleteFeedback) // DELETE /api/v1/feedbacks/{id} + r.Post("/", feedbackHandler.CreateFeedback) + r.Put("/{id}", feedbackHandler.UpdateFeedback) + r.Delete("/{id}", feedbackHandler.DeleteFeedback) + r.Get("/my", feedbackHandler.GetMyFeedbacks) - // Маршруты для комментариев (требуют аутентификации) - r.Post("/{id}/comments", feedbackHandler.AddComment) // POST /api/v1/feedbacks/{id}/comments - r.Put("/{id}/comments/{commentID}", feedbackHandler.UpdateComment) // PUT /api/v1/feedbacks/{id}/comments/{commentID} - r.Delete("/{id}/comments/{commentID}", feedbackHandler.DeleteComment) // DELETE /api/v1/feedbacks/{id}/comments/{commentID} - - // Маршруты для владельца (получение своих отзывов) - r.Get("/my", feedbackHandler.GetMyFeedbacks) // GET /api/v1/feedbacks/my + // Маршруты для комментариев + r.Post("/{id}/comments", feedbackHandler.AddComment) + r.Put("/{id}/comments/{commentID}", feedbackHandler.UpdateComment) + r.Delete("/{id}/comments/{commentID}", feedbackHandler.DeleteComment) }) }) } \ No newline at end of file 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 82ea5fb..4650e15 100644 --- a/main_dc/yalarba/api_yal/internal/domain/feetback/service.go +++ b/main_dc/yalarba/api_yal/internal/domain/feetback/service.go @@ -1,749 +1,317 @@ package feetback import ( - "context" - "errors" - "fmt" - "strings" - "api_yal/internal/models" "api_yal/internal/repository" - + "errors" "gorm.io/gorm" ) +// FeedbackService интерфейс сервиса отзывов 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) + Create(feedback *models.Feedback, AuthorID uint) error + GetByID(id uint) (*models.Feedback, error) + Update(id uint, AuthorID uint, updates map[string]interface{}) error + Delete(id uint, AuthorID uint, isAdmin bool) error + List(offset, limit int) ([]models.Feedback, int64, error) + ListByOwner(ownerID uint, offset, limit int) ([]models.Feedback, int64, error) + ListByObject(objectID uint, offset, limit int) ([]models.Feedback, int64, error) + ListByPlatform(platform models.PlatformType, offset, limit int) ([]models.Feedback, int64, error) + Search(query string, offset, limit int) ([]models.Feedback, int64, error) + GetComments(feedbackID uint, offset, limit int) ([]models.Comment, int, error) + AddComment(feedbackID uint, AuthorID uint, text string) (*models.Comment, error) + UpdateComment(commentID uint, AuthorID uint, text string, isAdmin bool) error + DeleteComment(commentID uint, AuthorID uint, isAdmin bool) error + GetStats() (*FeedbackStatsResponse, error) } -type feedbackServiceImpl struct { - feedbackRepository repository.FeedbackRepository +// FeedbackServiceImpl реализация сервиса отзывов +type FeedbackServiceImpl struct { + feedbackRepo repository.FeedbackRepository + db *gorm.DB } -func NewFeedbackServiceImpl(feedbackRepository repository.FeedbackRepository) FeedbackService { - return &feedbackServiceImpl{ - feedbackRepository: feedbackRepository, +// NewFeedbackServiceImpl создает новый экземпляр сервиса +func NewFeedbackServiceImpl(feedbackRepo repository.FeedbackRepository, db *gorm.DB) FeedbackService { + return &FeedbackServiceImpl{ + feedbackRepo: feedbackRepo, + db: db, } } // 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, - } - - // Устанавливаем начальное значение счетчика комментариев +func (s *FeedbackServiceImpl) Create(feedback *models.Feedback, AuthorID uint) error { + feedback.OwnerID = AuthorID 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 + return s.feedbackRepo.Create(feedback) } // 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 +func (s *FeedbackServiceImpl) GetByID(id uint) (*models.Feedback, error) { + return s.feedbackRepo.GetByID(id) } -// 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 -} - -// Добавьте в конец файла 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) +// Update обновляет отзыв +func (s *FeedbackServiceImpl) Update(id uint, AuthorID uint, updates map[string]interface{}) error { + // Получаем существующий отзыв + feedback, err := s.feedbackRepo.GetByID(id) if err != nil { return err } - - return s.feedbackRepository.UpdateCommentCount(feedbackID, count) + + // Проверяем права (только владелец может редактировать) + if feedback.OwnerID != AuthorID { + return errors.New("you can only update your own feedback") + } + + // Применяем обновления + return s.db.Model(&models.Feedback{}).Where("id = ?", id).Updates(updates).Error +} + +// Delete удаляет отзыв +func (s *FeedbackServiceImpl) Delete(id uint, AuthorID uint, isAdmin bool) error { + // Получаем существующий отзыв + feedback, err := s.feedbackRepo.GetByID(id) + if err != nil { + return err + } + + // Проверяем права (владелец или админ) + if feedback.OwnerID != AuthorID && !isAdmin { + return errors.New("you can only delete your own feedback") + } + + return s.feedbackRepo.Delete(id) +} + +// List возвращает список отзывов с пагинацией +func (s *FeedbackServiceImpl) List(offset, limit int) ([]models.Feedback, int64, error) { + if limit <= 0 || limit > 100 { + limit = 20 + } + if offset < 0 { + offset = 0 + } + + feedbacks, err := s.feedbackRepo.List(offset, limit) + if err != nil { + return nil, 0, err + } + + total, err := s.feedbackRepo.Count() + if err != nil { + return nil, 0, err + } + + return feedbacks, total, nil +} + +// ListByOwner возвращает отзывы по владельцу +func (s *FeedbackServiceImpl) ListByOwner(ownerID uint, offset, limit int) ([]models.Feedback, int64, error) { + if limit <= 0 || limit > 100 { + limit = 20 + } + if offset < 0 { + offset = 0 + } + + feedbacks, err := s.feedbackRepo.ListByOwner(ownerID, offset, limit) + if err != nil { + return nil, 0, err + } + + // Считаем общее количество отзывов пользователя + var total int64 + err = s.db.Model(&models.Feedback{}).Where("owner_id = ?", ownerID).Count(&total).Error + + return feedbacks, total, err +} + +// ListByObject возвращает отзывы по объекту +func (s *FeedbackServiceImpl) ListByObject(objectID uint, offset, limit int) ([]models.Feedback, int64, error) { + if limit <= 0 || limit > 100 { + limit = 20 + } + if offset < 0 { + offset = 0 + } + + feedbacks, err := s.feedbackRepo.ListByObject(objectID, offset, limit) + if err != nil { + return nil, 0, err + } + + var total int64 + err = s.db.Model(&models.Feedback{}).Where("object_id = ?", objectID).Count(&total).Error + + return feedbacks, total, err +} + +// ListByPlatform возвращает отзывы по платформе +func (s *FeedbackServiceImpl) ListByPlatform(platform models.PlatformType, offset, limit int) ([]models.Feedback, int64, error) { + if limit <= 0 || limit > 100 { + limit = 20 + } + if offset < 0 { + offset = 0 + } + + feedbacks, err := s.feedbackRepo.ListByPlatform(platform, offset, limit) + if err != nil { + return nil, 0, err + } + + var total int64 + err = s.db.Model(&models.Feedback{}).Where("platform = ?", platform).Count(&total).Error + + return feedbacks, total, err +} + +// Search ищет отзывы по тексту +func (s *FeedbackServiceImpl) Search(query string, offset, limit int) ([]models.Feedback, int64, error) { + if limit <= 0 || limit > 100 { + limit = 20 + } + if offset < 0 { + offset = 0 + } + + feedbacks, err := s.feedbackRepo.Search(query, offset, limit) + if err != nil { + return nil, 0, err + } + + var total int64 + err = s.db.Model(&models.Feedback{}).Where("text LIKE ?", "%"+query+"%").Count(&total).Error + + return feedbacks, total, err +} + +// GetComments возвращает комментарии к отзыву +func (s *FeedbackServiceImpl) GetComments(feedbackID uint, offset, limit int) ([]models.Comment, int, error) { + if limit <= 0 || limit > 50 { + limit = 20 + } + if offset < 0 { + offset = 0 + } + + comments, err := s.feedbackRepo.GetComments(feedbackID, offset, limit) + if err != nil { + return nil, 0, err + } + + total, err := s.feedbackRepo.GetCommentCount(feedbackID) + if err != nil { + return nil, 0, err + } + + return comments, total, err +} + +// AddComment добавляет комментарий к отзыву +func (s *FeedbackServiceImpl) AddComment(feedbackID uint, AuthorID uint, text string) (*models.Comment, error) { + // Проверяем существование отзыва + _, err := s.feedbackRepo.GetByID(feedbackID) + if err != nil { + return nil, errors.New("feedback not found") + } + + // Создаем комментарий + comment := &models.Comment{ + FeedbackID: feedbackID, + AuthorID: AuthorID, // Используем AuthorID, а не OwnerID + Text: text, + } + + err = s.db.Create(comment).Error + if err != nil { + return nil, err + } + + // Загружаем аккаунт для комментария + s.db.Preload("Account").First(comment, comment.ID) + + // Обновляем счетчик комментариев + newCount, _ := s.feedbackRepo.GetCommentCount(feedbackID) + s.feedbackRepo.UpdateCommentCount(feedbackID, newCount) + + return comment, nil +} + +// UpdateComment обновляет комментарий +func (s *FeedbackServiceImpl) UpdateComment(commentID uint, AuthorID uint, text string, isAdmin bool) error { + var comment models.Comment + err := s.db.First(&comment, commentID).Error + if err != nil { + return err + } + + // Проверяем права (владелец или админ) + if comment.AuthorID != AuthorID && !isAdmin { + return errors.New("you can only update your own comments") + } + + return s.db.Model(&comment).Update("text", text).Error +} + +// DeleteComment удаляет комментарий +func (s *FeedbackServiceImpl) DeleteComment(commentID uint, AuthorID uint, isAdmin bool) error { + var comment models.Comment + err := s.db.First(&comment, commentID).Error + if err != nil { + return err + } + + // Проверяем права (владелец или админ) + if comment.AuthorID != AuthorID && !isAdmin { + return errors.New("you can only delete your own comments") + } + + feedbackID := comment.FeedbackID + + err = s.db.Delete(&comment).Error + if err != nil { + return err + } + + // Обновляем счетчик комментариев у отзыва + newCount, _ := s.feedbackRepo.GetCommentCount(feedbackID) + s.feedbackRepo.UpdateCommentCount(feedbackID, newCount) + + return nil +} + +// GetStats возвращает статистику по отзывам +func (s *FeedbackServiceImpl) GetStats() (*FeedbackStatsResponse, error) { + stats := &FeedbackStatsResponse{ + RatingDistribution: make(map[int]int64), + PlatformStats: make(map[models.PlatformType]int64), + } + + // Общее количество + s.db.Model(&models.Feedback{}).Count(&stats.TotalFeedbacks) + + // Средний рейтинг + var avgRating float64 + s.db.Model(&models.Feedback{}).Select("COALESCE(AVG(rating), 0)").Row().Scan(&avgRating) + stats.AverageRating = avgRating + + // Распределение рейтингов + for i := 1; i <= 5; i++ { + var count int64 + s.db.Model(&models.Feedback{}).Where("rating = ?", i).Count(&count) + stats.RatingDistribution[i] = count + } + + // Статистика по платформам + platforms := []models.PlatformType{"google", "yandex", "2gis", "whatsapp", "telegram", "vk"} + for _, platform := range platforms { + var count int64 + s.db.Model(&models.Feedback{}).Where("platform = ?", platform).Count(&count) + stats.PlatformStats[platform] = count + } + + return stats, nil } \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/models/feedback.go b/main_dc/yalarba/api_yal/internal/models/feedback.go index f209303..4c65786 100644 --- a/main_dc/yalarba/api_yal/internal/models/feedback.go +++ b/main_dc/yalarba/api_yal/internal/models/feedback.go @@ -8,11 +8,11 @@ type Feedback struct { // owner account ID OwnerID uint `gorm:"not null;index" json:"owner_id"` - Owner Account `gorm:"foreignKey:OwnerID;references:ID" json:"owner,omitempty"` + Owner Account `gorm:"foreignKey:OwnerID;references:ID" json:"owner"` // object IO ObjectID uint `gorm:"not null;index" json:"object_id"` - Object Object `gorm:"foreignKey:ObjectID;references:ID" json:"object,omitempty"` + Object Object `gorm:"foreignKey:ObjectID;references:ID" json:"object"` // enterprener / tourist Platform PlatformType `json:"platform"` diff --git a/main_dc/yalarba/api_yal/internal/repository/comment_repository.go b/main_dc/yalarba/api_yal/internal/repository/comment_repository.go index daf27b9..1212bd7 100644 --- a/main_dc/yalarba/api_yal/internal/repository/comment_repository.go +++ b/main_dc/yalarba/api_yal/internal/repository/comment_repository.go @@ -51,4 +51,6 @@ type CommentRepository interface { // ToggleVerification переключает статус верификации комментария ToggleVerification(id uint, verified bool) error + + } \ 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 5d94dfb..7426ede 100644 --- a/main_dc/yalarba/api_yal/internal/repository/feedback_repository.go +++ b/main_dc/yalarba/api_yal/internal/repository/feedback_repository.go @@ -55,23 +55,5 @@ type FeedbackRepository interface { // Добавьте в интерфейс 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 c4e3675..807ce54 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 @@ -158,126 +158,3 @@ func (r *feedbackRepositoryImpl) getObjectByID(id uint) (*models.Object, error) return &object, nil } -// 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 -} diff --git a/main_dc/yalarba/api_yal/internal/router/router.go b/main_dc/yalarba/api_yal/internal/router/router.go index 9da5d60..27be44c 100644 --- a/main_dc/yalarba/api_yal/internal/router/router.go +++ b/main_dc/yalarba/api_yal/internal/router/router.go @@ -2,9 +2,10 @@ package router import ( "api_yal/internal/config" - "api_yal/internal/logger" - "api_yal/internal/domain/auth" "api_yal/internal/domain/account" + "api_yal/internal/domain/auth" + "api_yal/internal/domain/feetback" + "api_yal/internal/logger" "time" "encoding/json" @@ -27,7 +28,7 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { // ВСЕ middleware должны быть определены ДО маршрутов // 1. Сначала добавляем production middleware addProductionMiddleware(r, config) - + // 2. Затем добавляем middleware аутентификации (он тоже применяется ко всем маршрутам) zapLogger.Debug("Auth middleware применён") @@ -36,7 +37,7 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - + if err := json.NewEncoder(w).Encode(map[string]string{"status": "healthy"}); err != nil { zapLogger.Error("Ошибка при отправке health check ответа", zap.String("path", r.URL.Path), @@ -50,9 +51,13 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { r.Route("/api/v1", func(r chi.Router) { // Регистрируем маршруты аутентификации auth.RegisterRoutes(r, db, config.JWTSecret) - + // Регистрируем маршруты аккаунтов account.RegisterRoutes(r, db, config.JWTSecret) + + + // Регистрируем маршруты отзывов + feetback.RegisterRoutes(r, db, config.JWTSecret) // Добавьте эту строку }) zapLogger.Info("Настройка маршрутов завершена") @@ -74,7 +79,7 @@ func addProductionMiddleware(r *chi.Mux, config *config.Config) { r.Use(ChiMiddleware.Timeout(30 * time.Second)) r.Use(ChiMiddleware.Compress(5, "gzip")) r.Use(ChiMiddleware.StripSlashes) - + // CORS r.Use(cors.Handler(cors.Options{ AllowedOrigins: config.CORS.AllowedOrigins, @@ -84,15 +89,15 @@ func addProductionMiddleware(r *chi.Mux, config *config.Config) { AllowCredentials: true, MaxAge: 300, })) - + // Content-Type проверка r.Use(ChiMiddleware.AllowContentType("application/json", "application/xml")) - + // Rate limiting if config.RateLimit.Enabled { r.Use(ChiMiddleware.Throttle(config.RateLimit.RequestsPerSecond)) } - + // Security headers r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -100,12 +105,12 @@ func addProductionMiddleware(r *chi.Mux, config *config.Config) { w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") - + if config.Environment == "production" { w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") } - + next.ServeHTTP(w, r) }) }) -} \ No newline at end of file +}