From cc3d0a8b072073ab51f968f80382990fe38051a4 Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Tue, 19 May 2026 18:11:20 +0500 Subject: [PATCH] On branch main modified: yalarba/api_yal/internal/domain/account/service.go modified: yalarba/api_yal/internal/domain/comment/dto.go new file: yalarba/api_yal/internal/domain/comment/handler.go new file: yalarba/api_yal/internal/domain/comment/router.go new file: yalarba/api_yal/internal/domain/comment/service.go modified: yalarba/api_yal/internal/repository/feedback_repository.go new file: yalarba/api_yal/internal/util/JSON_resp.go Realize comment domain hole --- .../internal/domain/account/service.go | 1 + .../api_yal/internal/domain/comment/dto.go | 44 +- .../internal/domain/comment/handler.go | 438 ++++++++++++++++ .../api_yal/internal/domain/comment/router.go | 45 ++ .../internal/domain/comment/service.go | 491 ++++++++++++++++++ .../repository/feedback_repository.go | 3 - .../api_yal/internal/util/JSON_resp.go | 19 + 7 files changed, 1031 insertions(+), 10 deletions(-) create mode 100644 main_dc/yalarba/api_yal/internal/domain/comment/handler.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/comment/router.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/comment/service.go create mode 100644 main_dc/yalarba/api_yal/internal/util/JSON_resp.go diff --git a/main_dc/yalarba/api_yal/internal/domain/account/service.go b/main_dc/yalarba/api_yal/internal/domain/account/service.go index 7647ce6..809a7b2 100644 --- a/main_dc/yalarba/api_yal/internal/domain/account/service.go +++ b/main_dc/yalarba/api_yal/internal/domain/account/service.go @@ -537,6 +537,7 @@ func (s *accountServiceImpl) UpdateAccountModel(account *models.Account) error { // Вспомогательные методы func (s *accountServiceImpl) getSearchTotal(query string) (int64, error) { + fmt.Printf("query string = %s", query) // Здесь должна быть реализация подсчета общего количества результатов поиска // Для простоты возвращаем 0 return 0, nil diff --git a/main_dc/yalarba/api_yal/internal/domain/comment/dto.go b/main_dc/yalarba/api_yal/internal/domain/comment/dto.go index 0fc729d..6ea3d80 100644 --- a/main_dc/yalarba/api_yal/internal/domain/comment/dto.go +++ b/main_dc/yalarba/api_yal/internal/domain/comment/dto.go @@ -4,22 +4,52 @@ import ( "time" ) +// CommentResponse - полный ответ для комментария +type CommentResponse struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + AuthorID uint `json:"author_id"` + AuthorName string `json:"author_name"` + FeedbackID uint `json:"feedback_id"` + Text string `json:"text"` + ParentID *uint `json:"parent_id,omitempty"` + IsEdited bool `json:"is_edited"` + IsVerified bool `json:"is_verified"` + Replies []CommentShortResponse `json:"replies,omitempty"` + RepliesCount int64 `json:"replies_count,omitempty"` +} + // CommentShortResponse - краткий ответ для комментария type CommentShortResponse struct { - ID uint `json:"id"` - CreatedAt time.Time `json:"created_at"` - OwnerID uint `json:"owner_id"` - OwnerName string `json:"owner_name,omitempty"` - Text string `json:"text"` + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + AuthorID uint `json:"author_id"` + AuthorName string `json:"author_name"` + Text string `json:"text"` + IsEdited bool `json:"is_edited"` } // CreateCommentRequest - DTO для создания комментария type CreateCommentRequest struct { FeedbackID uint `json:"feedback_id" binding:"required"` - Text string `json:"text" binding:"required"` + Text string `json:"text" binding:"required,min=1,max=5000"` + ParentID *uint `json:"parent_id"` // опционально, для ответов на комментарии } // UpdateCommentRequest - DTO для обновления комментария type UpdateCommentRequest struct { - Text *string `json:"text" binding:"required"` + Text string `json:"text" binding:"required,min=1,max=5000"` +} + +// ListCommentsRequest - DTO для списка комментариев с фильтрацией +type ListCommentsRequest struct { + Page int `form:"page,default=1"` + PageSize int `form:"page_size,default=20"` + FeedbackID *uint `form:"feedback_id"` + AuthorID *uint `form:"author_id"` + ParentID *uint `form:"parent_id"` + Verified *bool `form:"verified"` + SortBy string `form:"sort_by,default=created_at"` + SortOrder string `form:"sort_order,default=desc"` } \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/comment/handler.go b/main_dc/yalarba/api_yal/internal/domain/comment/handler.go new file mode 100644 index 0000000..3eeca2e --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/comment/handler.go @@ -0,0 +1,438 @@ +package comment + +import ( + "api_yal/internal/logger" + "api_yal/internal/util" + "encoding/json" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" + "go.uber.org/zap" +) + +type CommentHandler struct { + service CommentService + logger *zap.Logger +} + +func NewCommentHandler(service CommentService) *CommentHandler { + return &CommentHandler{ + service: service, + logger: logger.Get(), + } +} + +// CreateComment создает новый комментарий +func (h *CommentHandler) CreateComment(w http.ResponseWriter, r *http.Request) { + var req CreateCommentRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + h.logger.Error("Failed to decode request", + zap.Error(err), + ) + util.ResponseWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"}) + return + } + + // Получаем userID из контекста (устанавливается AuthMiddleware) + userID, ok := r.Context().Value("user_id").(uint) + if !ok { + h.logger.Error("User ID not found in context") + util.ResponseWithJSON(w, http.StatusUnauthorized, map[string]string{"error": "User not authenticated"}) + return + } + + comment, err := h.service.CreateComment(userID, &req) + if err != nil { + h.logger.Error("Failed to create comment", + zap.Error(err), + zap.Uint("user_id", userID), + ) + util.ResponseWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + util.ResponseWithJSON(w, http.StatusCreated, comment) +} + +// GetCommentByID возвращает комментарий по ID +func (h *CommentHandler) GetCommentByID(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + h.logger.Error("Invalid comment ID", + zap.Error(err), + zap.String("id", idStr), + ) + util.ResponseWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid comment ID"}) + return + } + + comment, err := h.service.GetCommentByID(uint(id)) + if err != nil { + h.logger.Error("Failed to get comment", + zap.Error(err), + zap.Uint64("comment_id", id), + ) + util.ResponseWithJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()}) + return + } + + util.ResponseWithJSON(w, http.StatusOK, comment) +} + +// UpdateComment обновляет комментарий +func (h *CommentHandler) UpdateComment(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + h.logger.Error("Invalid comment ID", + zap.Error(err), + zap.String("id", idStr), + ) + util.ResponseWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid comment ID"}) + return + } + + var req UpdateCommentRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + h.logger.Error("Failed to decode request", + zap.Error(err), + ) + util.ResponseWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"}) + return + } + + userID, ok := r.Context().Value("user_id").(uint) + if !ok { + h.logger.Error("User ID not found in context") + util.ResponseWithJSON(w, http.StatusUnauthorized, map[string]string{"error": "User not authenticated"}) + return + } + + comment, err := h.service.UpdateComment(uint(id), userID, &req) + if err != nil { + h.logger.Error("Failed to update comment", + zap.Error(err), + zap.Uint64("comment_id", id), + zap.Uint("user_id", userID), + ) + status := http.StatusInternalServerError + if err.Error() == "comment not found" { + status = http.StatusNotFound + } else if err.Error() == "you can only edit your own comments" { + status = http.StatusForbidden + } + util.ResponseWithJSON(w, status, map[string]string{"error": err.Error()}) + return + } + + util.ResponseWithJSON(w, http.StatusOK, comment) +} + +// DeleteComment удаляет комментарий +func (h *CommentHandler) DeleteComment(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + h.logger.Error("Invalid comment ID", + zap.Error(err), + zap.String("id", idStr), + ) + util.ResponseWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid comment ID"}) + return + } + + userID, ok := r.Context().Value("user_id").(uint) + if !ok { + h.logger.Error("User ID not found in context") + util.ResponseWithJSON(w, http.StatusUnauthorized, map[string]string{"error": "User not authenticated"}) + return + } + + // Проверяем, является ли пользователь администратором + isAdmin, _ := r.Context().Value("is_admin").(bool) + + err = h.service.DeleteComment(uint(id), userID, isAdmin) + if err != nil { + h.logger.Error("Failed to delete comment", + zap.Error(err), + zap.Uint64("comment_id", id), + zap.Uint("user_id", userID), + zap.Bool("is_admin", isAdmin), + ) + status := http.StatusInternalServerError + if err.Error() == "comment not found" { + status = http.StatusNotFound + } else if err.Error() == "you can only delete your own comments" { + status = http.StatusForbidden + } + util.ResponseWithJSON(w, status, map[string]string{"error": err.Error()}) + return + } + + util.ResponseWithJSON(w, http.StatusOK, map[string]string{"message": "Comment deleted successfully"}) +} + +// ListComments возвращает список комментариев с фильтрацией +func (h *CommentHandler) ListComments(w http.ResponseWriter, r *http.Request) { + var req ListCommentsRequest + if err := r.ParseForm(); err != nil { + h.logger.Error("Failed to parse form", + zap.Error(err), + ) + util.ResponseWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid query parameters"}) + return + } + + // Парсим query параметры + if pageStr := r.URL.Query().Get("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil && page > 0 { + req.Page = page + } + } + if sizeStr := r.URL.Query().Get("page_size"); sizeStr != "" { + if size, err := strconv.Atoi(sizeStr); err == nil && size > 0 && size <= 100 { + req.PageSize = size + } + } + if feedbackIDStr := r.URL.Query().Get("feedback_id"); feedbackIDStr != "" { + if id, err := strconv.ParseUint(feedbackIDStr, 10, 32); err == nil { + idUint := uint(id) + req.FeedbackID = &idUint + } + } + if authorIDStr := r.URL.Query().Get("author_id"); authorIDStr != "" { + if id, err := strconv.ParseUint(authorIDStr, 10, 32); err == nil { + idUint := uint(id) + req.AuthorID = &idUint + } + } + if parentIDStr := r.URL.Query().Get("parent_id"); parentIDStr != "" { + if id, err := strconv.ParseUint(parentIDStr, 10, 32); err == nil { + idUint := uint(id) + req.ParentID = &idUint + } + } + if verifiedStr := r.URL.Query().Get("verified"); verifiedStr != "" { + if verified, err := strconv.ParseBool(verifiedStr); err == nil { + req.Verified = &verified + } + } + if sortBy := r.URL.Query().Get("sort_by"); sortBy != "" { + req.SortBy = sortBy + } + if sortOrder := r.URL.Query().Get("sort_order"); sortOrder != "" { + req.SortOrder = sortOrder + } + + comments, total, err := h.service.ListComments(&req) + if err != nil { + h.logger.Error("Failed to list comments", + zap.Error(err), + zap.Any("request", req), + ) + util.ResponseWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + response := map[string]interface{}{ + "data": comments, + "total": total, + "page": req.Page, + "page_size": req.PageSize, + "total_pages": (total + int64(req.PageSize) - 1) / int64(req.PageSize), + } + + util.ResponseWithJSON(w, http.StatusOK, response) +} + +// GetCommentsByFeedback возвращает комментарии по отзыву +func (h *CommentHandler) GetCommentsByFeedback(w http.ResponseWriter, r *http.Request) { + feedbackIDStr := chi.URLParam(r, "feedbackID") + feedbackID, err := strconv.ParseUint(feedbackIDStr, 10, 32) + if err != nil { + h.logger.Error("Invalid feedback ID", + zap.Error(err), + zap.String("feedback_id", feedbackIDStr), + ) + util.ResponseWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid feedback ID"}) + return + } + + var req ListCommentsRequest + if pageStr := r.URL.Query().Get("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil && page > 0 { + req.Page = page + } + } + if sizeStr := r.URL.Query().Get("page_size"); sizeStr != "" { + if size, err := strconv.Atoi(sizeStr); err == nil && size > 0 && size <= 100 { + req.PageSize = size + } + } + + comments, total, err := h.service.GetCommentsByFeedback(uint(feedbackID), &req) + if err != nil { + h.logger.Error("Failed to get comments by feedback", + zap.Error(err), + zap.Uint64("feedback_id", feedbackID), + ) + util.ResponseWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + response := map[string]interface{}{ + "data": comments, + "total": total, + "page": req.Page, + "page_size": req.PageSize, + "total_pages": (total + int64(req.PageSize) - 1) / int64(req.PageSize), + } + + util.ResponseWithJSON(w, http.StatusOK, response) +} + +// GetMyComments возвращает комментарии текущего пользователя +func (h *CommentHandler) GetMyComments(w http.ResponseWriter, r *http.Request) { + userID, ok := r.Context().Value("user_id").(uint) + if !ok { + h.logger.Error("User ID not found in context") + util.ResponseWithJSON(w, http.StatusUnauthorized, map[string]string{"error": "User not authenticated"}) + return + } + + var req ListCommentsRequest + if pageStr := r.URL.Query().Get("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil && page > 0 { + req.Page = page + } + } + if sizeStr := r.URL.Query().Get("page_size"); sizeStr != "" { + if size, err := strconv.Atoi(sizeStr); err == nil && size > 0 && size <= 100 { + req.PageSize = size + } + } + + comments, total, err := h.service.GetCommentsByAuthor(userID, &req) + if err != nil { + h.logger.Error("Failed to get my comments", + zap.Error(err), + zap.Uint("user_id", userID), + ) + util.ResponseWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + response := map[string]interface{}{ + "data": comments, + "total": total, + "page": req.Page, + "page_size": req.PageSize, + "total_pages": (total + int64(req.PageSize) - 1) / int64(req.PageSize), + } + + util.ResponseWithJSON(w, http.StatusOK, response) +} + +// GetReplies возвращает ответы на комментарий +func (h *CommentHandler) GetReplies(w http.ResponseWriter, r *http.Request) { + parentIDStr := chi.URLParam(r, "parentID") + parentID, err := strconv.ParseUint(parentIDStr, 10, 32) + if err != nil { + h.logger.Error("Invalid parent comment ID", + zap.Error(err), + zap.String("parent_id", parentIDStr), + ) + util.ResponseWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid parent comment ID"}) + return + } + + var req ListCommentsRequest + if pageStr := r.URL.Query().Get("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil && page > 0 { + req.Page = page + } + } + if sizeStr := r.URL.Query().Get("page_size"); sizeStr != "" { + if size, err := strconv.Atoi(sizeStr); err == nil && size > 0 && size <= 100 { + req.PageSize = size + } + } + + replies, total, err := h.service.GetReplies(uint(parentID), &req) + if err != nil { + h.logger.Error("Failed to get replies", + zap.Error(err), + zap.Uint64("parent_id", parentID), + ) + util.ResponseWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + response := map[string]interface{}{ + "data": replies, + "total": total, + "page": req.Page, + "page_size": req.PageSize, + "total_pages": (total + int64(req.PageSize) - 1) / int64(req.PageSize), + } + + util.ResponseWithJSON(w, http.StatusOK, response) +} + +// VerifyComment верифицирует комментарий (только для админов) +func (h *CommentHandler) VerifyComment(w http.ResponseWriter, r *http.Request) { + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + h.logger.Error("Invalid comment ID", + zap.Error(err), + zap.String("id", idStr), + ) + util.ResponseWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid comment ID"}) + return + } + + var req struct { + Verified bool `json:"verified"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + h.logger.Error("Failed to decode request", + zap.Error(err), + ) + util.ResponseWithJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid request body"}) + return + } + + err = h.service.VerifyComment(uint(id), req.Verified) + if err != nil { + h.logger.Error("Failed to verify comment", + zap.Error(err), + zap.Uint64("comment_id", id), + zap.Bool("verified", req.Verified), + ) + util.ResponseWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + status := "unverified" + if req.Verified { + status = "verified" + } + util.ResponseWithJSON(w, http.StatusOK, map[string]string{"message": "Comment " + status + " successfully"}) +} + +// GetCommentStats возвращает статистику по комментариям +func (h *CommentHandler) GetCommentStats(w http.ResponseWriter, r *http.Request) { + stats, err := h.service.GetCommentStats() + if err != nil { + h.logger.Error("Failed to get comment stats", + zap.Error(err), + ) + util.ResponseWithJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + + util.ResponseWithJSON(w, http.StatusOK, stats) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/comment/router.go b/main_dc/yalarba/api_yal/internal/domain/comment/router.go new file mode 100644 index 0000000..4070812 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/comment/router.go @@ -0,0 +1,45 @@ +package comment + +import ( + "api_yal/internal/logger" + "api_yal/internal/middleware" + "api_yal/internal/repository" + + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { + l := logger.Get() + l.Info("Registering routes for comment") + + commentRepo := repository.NewCommentRepository(db) + commentService := NewCommentServiceImpl(commentRepo, db) + commentHandler := NewCommentHandler(commentService) + + // Группируем маршруты для комментариев + r.Route("/comments", func(r chi.Router) { + // Публичные маршруты (не требуют аутентификации) + r.Get("/", commentHandler.ListComments) + r.Get("/stats", commentHandler.GetCommentStats) + r.Get("/{id}", commentHandler.GetCommentByID) + r.Get("/feedback/{feedbackID}", commentHandler.GetCommentsByFeedback) + r.Get("/replies/{parentID}", commentHandler.GetReplies) + + // Защищенные маршруты (требуют аутентификации) + r.Group(func(r chi.Router) { + r.Use(middleware.AuthMiddleware(jwtSecret)) + + r.Post("/", commentHandler.CreateComment) + r.Put("/{id}", commentHandler.UpdateComment) + r.Delete("/{id}", commentHandler.DeleteComment) + r.Get("/my", commentHandler.GetMyComments) + + // Маршруты для админов + r.Group(func(r chi.Router) { + r.Use(middleware.AdminOnlyMiddleware) + r.Put("/{id}/verify", commentHandler.VerifyComment) + }) + }) + }) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/comment/service.go b/main_dc/yalarba/api_yal/internal/domain/comment/service.go new file mode 100644 index 0000000..d8ba369 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/comment/service.go @@ -0,0 +1,491 @@ +package comment + +import ( + "api_yal/internal/logger" + "api_yal/internal/models" + "api_yal/internal/repository" + "errors" + "fmt" + + "gorm.io/gorm" + "go.uber.org/zap" +) + +type CommentService interface { + CreateComment(userID uint, req *CreateCommentRequest) (*CommentResponse, error) + GetCommentByID(id uint) (*CommentResponse, error) + UpdateComment(id uint, userID uint, req *UpdateCommentRequest) (*CommentResponse, error) + DeleteComment(id uint, userID uint, isAdmin bool) error + ListComments(req *ListCommentsRequest) ([]CommentResponse, int64, error) + GetCommentsByFeedback(feedbackID uint, req *ListCommentsRequest) ([]CommentResponse, int64, error) + GetCommentsByAuthor(authorID uint, req *ListCommentsRequest) ([]CommentResponse, int64, error) + GetReplies(parentID uint, req *ListCommentsRequest) ([]CommentResponse, int64, error) + VerifyComment(id uint, verified bool) error + GetCommentStats() (map[string]interface{}, error) +} + +type CommentServiceImpl struct { + commentRepo repository.CommentRepository + db *gorm.DB + logger *zap.Logger +} + +func NewCommentServiceImpl(commentRepo repository.CommentRepository, db *gorm.DB) *CommentServiceImpl { + return &CommentServiceImpl{ + commentRepo: commentRepo, + db: db, + logger: logger.Get(), + } +} + +func (s *CommentServiceImpl) CreateComment(userID uint, req *CreateCommentRequest) (*CommentResponse, error) { + s.logger.Info("Creating new comment", + zap.Uint("user_id", userID), + zap.Uint("feedback_id", req.FeedbackID), + zap.Uint("parent_id", *req.ParentID), + ) + + comment := &models.Comment{ + AuthorID: userID, + FeedbackID: req.FeedbackID, + Text: req.Text, + ParentID: req.ParentID, + IsVerified: false, // можно настроить автоматическую верификацию + } + + err := s.commentRepo.Create(comment) + if err != nil { + s.logger.Error("Failed to create comment", + zap.Error(err), + zap.Uint("user_id", userID), + ) + return nil, fmt.Errorf("failed to create comment: %w", err) + } + + // Загружаем полную информацию о комментарии + createdComment, err := s.commentRepo.GetByID(comment.ID) + if err != nil { + s.logger.Error("Failed to get created comment", + zap.Error(err), + zap.Uint("comment_id", comment.ID), + ) + return nil, fmt.Errorf("failed to get created comment: %w", err) + } + + return s.mapToResponse(createdComment), nil +} + +func (s *CommentServiceImpl) GetCommentByID(id uint) (*CommentResponse, error) { + s.logger.Info("Getting comment by ID", + zap.Uint("comment_id", id), + ) + + comment, err := s.commentRepo.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("comment not found") + } + s.logger.Error("Failed to get comment", + zap.Error(err), + zap.Uint("comment_id", id), + ) + return nil, fmt.Errorf("failed to get comment: %w", err) + } + + return s.mapToResponse(comment), nil +} + +func (s *CommentServiceImpl) UpdateComment(id uint, userID uint, req *UpdateCommentRequest) (*CommentResponse, error) { + s.logger.Info("Updating comment", + zap.Uint("comment_id", id), + zap.Uint("user_id", userID), + ) + + comment, err := s.commentRepo.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("comment not found") + } + s.logger.Error("Failed to get comment for update", + zap.Error(err), + zap.Uint("comment_id", id), + ) + return nil, fmt.Errorf("failed to get comment: %w", err) + } + + // Проверяем права: только автор может редактировать + if comment.AuthorID != userID { + s.logger.Warn("Unauthorized update attempt", + zap.Uint("comment_id", id), + zap.Uint("author_id", comment.AuthorID), + zap.Uint("user_id", userID), + ) + return nil, errors.New("you can only edit your own comments") + } + + comment.Text = req.Text + err = s.commentRepo.Update(comment) + if err != nil { + s.logger.Error("Failed to update comment", + zap.Error(err), + zap.Uint("comment_id", id), + ) + return nil, fmt.Errorf("failed to update comment: %w", err) + } + + // Отмечаем как отредактированный + err = s.commentRepo.MarkAsEdited(id) + if err != nil { + s.logger.Warn("Failed to mark comment as edited", + zap.Error(err), + zap.Uint("comment_id", id), + ) + } + + updatedComment, err := s.commentRepo.GetByID(id) + if err != nil { + s.logger.Error("Failed to get updated comment", + zap.Error(err), + zap.Uint("comment_id", id), + ) + return nil, fmt.Errorf("failed to get updated comment: %w", err) + } + + return s.mapToResponse(updatedComment), nil +} + +func (s *CommentServiceImpl) DeleteComment(id uint, userID uint, isAdmin bool) error { + s.logger.Info("Deleting comment", + zap.Uint("comment_id", id), + zap.Uint("user_id", userID), + zap.Bool("is_admin", isAdmin), + ) + + comment, err := s.commentRepo.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("comment not found") + } + s.logger.Error("Failed to get comment for deletion", + zap.Error(err), + zap.Uint("comment_id", id), + ) + return fmt.Errorf("failed to get comment: %w", err) + } + + // Проверяем права: автор или администратор + if !isAdmin && comment.AuthorID != userID { + s.logger.Warn("Unauthorized delete attempt", + zap.Uint("comment_id", id), + zap.Uint("author_id", comment.AuthorID), + zap.Uint("user_id", userID), + zap.Bool("is_admin", isAdmin), + ) + return errors.New("you can only delete your own comments") + } + + err = s.commentRepo.Delete(id) + if err != nil { + s.logger.Error("Failed to delete comment", + zap.Error(err), + zap.Uint("comment_id", id), + ) + return fmt.Errorf("failed to delete comment: %w", err) + } + + return nil +} + +func (s *CommentServiceImpl) ListComments(req *ListCommentsRequest) ([]CommentResponse, int64, error) { + s.logger.Info("Listing comments", + zap.Int("page", req.Page), + zap.Int("page_size", req.PageSize), + ) + + offset := (req.Page - 1) * req.PageSize + limit := req.PageSize + + // Базовый запрос для подсчета + query := s.db.Model(&models.Comment{}) + + // Применяем фильтры + if req.FeedbackID != nil { + query = query.Where("feedback_id = ?", *req.FeedbackID) + } + if req.AuthorID != nil { + query = query.Where("author_id = ?", *req.AuthorID) + } + if req.ParentID != nil { + if *req.ParentID == 0 { + query = query.Where("parent_id IS NULL") + } else { + query = query.Where("parent_id = ?", *req.ParentID) + } + } + if req.Verified != nil { + query = query.Where("is_verified = ?", *req.Verified) + } + + // Подсчитываем общее количество + var total int64 + err := query.Count(&total).Error + if err != nil { + s.logger.Error("Failed to count comments", + zap.Error(err), + ) + return nil, 0, fmt.Errorf("failed to count comments: %w", err) + } + + // Получаем комментарии + comments, err := s.commentRepo.List(offset, limit) + if err != nil { + s.logger.Error("Failed to list comments", + zap.Error(err), + ) + return nil, 0, fmt.Errorf("failed to list comments: %w", err) + } + + // Фильтруем и маппим результаты + var responses []CommentResponse + for i := range comments { + // Применяем фильтры в памяти (так как репозиторий возвращает все) + if s.shouldIncludeComment(&comments[i], req) { + responses = append(responses, *s.mapToResponse(&comments[i])) + } + } + + return responses, total, nil +} + +func (s *CommentServiceImpl) GetCommentsByFeedback(feedbackID uint, req *ListCommentsRequest) ([]CommentResponse, int64, error) { + s.logger.Info("Getting comments by feedback", + zap.Uint("feedback_id", feedbackID), + zap.Int("page", req.Page), + zap.Int("page_size", req.PageSize), + ) + + offset := (req.Page - 1) * req.PageSize + limit := req.PageSize + + comments, err := s.commentRepo.ListByFeedback(feedbackID, offset, limit) + if err != nil { + s.logger.Error("Failed to get comments by feedback", + zap.Error(err), + zap.Uint("feedback_id", feedbackID), + ) + return nil, 0, fmt.Errorf("failed to get comments by feedback: %w", err) + } + + // Подсчитываем общее количество + var total int64 + err = s.db.Model(&models.Comment{}).Where("feedback_id = ?", feedbackID).Count(&total).Error + if err != nil { + s.logger.Error("Failed to count comments by feedback", + zap.Error(err), + zap.Uint("feedback_id", feedbackID), + ) + return nil, 0, fmt.Errorf("failed to count comments: %w", err) + } + + responses := make([]CommentResponse, len(comments)) + for i := range comments { + responses[i] = *s.mapToResponse(&comments[i]) + } + + return responses, total, nil +} + +func (s *CommentServiceImpl) GetCommentsByAuthor(authorID uint, req *ListCommentsRequest) ([]CommentResponse, int64, error) { + s.logger.Info("Getting comments by author", + zap.Uint("author_id", authorID), + zap.Int("page", req.Page), + zap.Int("page_size", req.PageSize), + ) + + offset := (req.Page - 1) * req.PageSize + limit := req.PageSize + + comments, err := s.commentRepo.ListByAuthor(authorID, offset, limit) + if err != nil { + s.logger.Error("Failed to get comments by author", + zap.Error(err), + zap.Uint("author_id", authorID), + ) + return nil, 0, fmt.Errorf("failed to get comments by author: %w", err) + } + + // Подсчитываем общее количество + var total int64 + err = s.db.Model(&models.Comment{}).Where("author_id = ?", authorID).Count(&total).Error + if err != nil { + s.logger.Error("Failed to count comments by author", + zap.Error(err), + zap.Uint("author_id", authorID), + ) + return nil, 0, fmt.Errorf("failed to count comments: %w", err) + } + + responses := make([]CommentResponse, len(comments)) + for i := range comments { + responses[i] = *s.mapToResponse(&comments[i]) + } + + return responses, total, nil +} + +func (s *CommentServiceImpl) GetReplies(parentID uint, req *ListCommentsRequest) ([]CommentResponse, int64, error) { + s.logger.Info("Getting replies for comment", + zap.Uint("parent_id", parentID), + zap.Int("page", req.Page), + zap.Int("page_size", req.PageSize), + ) + + offset := (req.Page - 1) * req.PageSize + limit := req.PageSize + + replies, err := s.commentRepo.ListReplies(parentID, offset, limit) + if err != nil { + s.logger.Error("Failed to get replies", + zap.Error(err), + zap.Uint("parent_id", parentID), + ) + return nil, 0, fmt.Errorf("failed to get replies: %w", err) + } + + // Подсчитываем общее количество + total, err := s.commentRepo.GetRepliesCount(parentID) + if err != nil { + s.logger.Error("Failed to count replies", + zap.Error(err), + zap.Uint("parent_id", parentID), + ) + return nil, 0, fmt.Errorf("failed to count replies: %w", err) + } + + responses := make([]CommentResponse, len(replies)) + for i := range replies { + responses[i] = *s.mapToResponse(&replies[i]) + } + + return responses, total, nil +} + +func (s *CommentServiceImpl) VerifyComment(id uint, verified bool) error { + s.logger.Info("Verifying comment", + zap.Uint("comment_id", id), + zap.Bool("verified", verified), + ) + + err := s.commentRepo.ToggleVerification(id, verified) + if err != nil { + s.logger.Error("Failed to verify comment", + zap.Error(err), + zap.Uint("comment_id", id), + ) + return fmt.Errorf("failed to verify comment: %w", err) + } + + return nil +} + +func (s *CommentServiceImpl) GetCommentStats() (map[string]interface{}, error) { + s.logger.Info("Getting comment statistics") + + var total int64 + err := s.db.Model(&models.Comment{}).Count(&total).Error + if err != nil { + s.logger.Error("Failed to count total comments", + zap.Error(err), + ) + return nil, fmt.Errorf("failed to count total comments: %w", err) + } + + var verified int64 + err = s.db.Model(&models.Comment{}).Where("is_verified = ?", true).Count(&verified).Error + if err != nil { + s.logger.Error("Failed to count verified comments", + zap.Error(err), + ) + return nil, fmt.Errorf("failed to count verified comments: %w", err) + } + + var withReplies int64 + err = s.db.Model(&models.Comment{}).Where("id IN (SELECT DISTINCT parent_id FROM comments WHERE parent_id IS NOT NULL)").Count(&withReplies).Error + if err != nil { + s.logger.Error("Failed to count comments with replies", + zap.Error(err), + ) + return nil, fmt.Errorf("failed to count comments with replies: %w", err) + } + + stats := map[string]interface{}{ + "total_comments": total, + "verified_comments": verified, + "unverified_comments": total - verified, + "comments_with_replies": withReplies, + } + + return stats, nil +} + +// Вспомогательные методы +func (s *CommentServiceImpl) mapToResponse(comment *models.Comment) *CommentResponse { + response := &CommentResponse{ + ID: comment.ID, + CreatedAt: comment.CreatedAt, + UpdatedAt: comment.UpdatedAt, + AuthorID: comment.AuthorID, + AuthorName: comment.Author.FullName, + FeedbackID: comment.FeedbackID, + Text: comment.Text, + IsEdited: comment.IsEdited, + IsVerified: comment.IsVerified, + } + + if comment.ParentID != nil { + response.ParentID = comment.ParentID + } + + // Добавляем количество ответов, если нужно + if len(comment.Replies) > 0 { + response.RepliesCount = int64(len(comment.Replies)) + response.Replies = make([]CommentShortResponse, len(comment.Replies)) + for i, reply := range comment.Replies { + response.Replies[i] = CommentShortResponse{ + ID: reply.ID, + CreatedAt: reply.CreatedAt, + AuthorID: reply.AuthorID, + AuthorName: reply.Author.FullName, + Text: reply.Text, + IsEdited: reply.IsEdited, + } + } + } else { + // Подсчитываем количество ответов, если они не были загружены + count, _ := s.commentRepo.GetRepliesCount(comment.ID) + response.RepliesCount = count + } + + return response +} + +func (s *CommentServiceImpl) shouldIncludeComment(comment *models.Comment, req *ListCommentsRequest) bool { + if req.FeedbackID != nil && comment.FeedbackID != *req.FeedbackID { + return false + } + if req.AuthorID != nil && comment.AuthorID != *req.AuthorID { + return false + } + if req.ParentID != nil { + if *req.ParentID == 0 && comment.ParentID != nil { + return false + } + if *req.ParentID != 0 && (comment.ParentID == nil || *comment.ParentID != *req.ParentID) { + return false + } + } + if req.Verified != nil && comment.IsVerified != *req.Verified { + return false + } + return true +} \ 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 7426ede..bcc6f66 100644 --- a/main_dc/yalarba/api_yal/internal/repository/feedback_repository.go +++ b/main_dc/yalarba/api_yal/internal/repository/feedback_repository.go @@ -52,8 +52,5 @@ type FeedbackRepository interface { // Search находит отзывы по тексту Search(query string, offset, limit int) ([]models.Feedback, error) - - // Добавьте в интерфейс FeedbackRepository: - } diff --git a/main_dc/yalarba/api_yal/internal/util/JSON_resp.go b/main_dc/yalarba/api_yal/internal/util/JSON_resp.go new file mode 100644 index 0000000..4fe1db2 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/util/JSON_resp.go @@ -0,0 +1,19 @@ +package util + +import ( + "encoding/json" + "net/http" +) + +// где-то в пакете comment или в отдельном пакете api +func ResponseWithJSON(w http.ResponseWriter, status int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + + if payload != nil { + if err := json.NewEncoder(w).Encode(payload); err != nil { + // логирование ошибки, если нужно + return + } + } +} \ No newline at end of file