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 }