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
This commit is contained in:
2026-05-19 18:11:20 +05:00
parent 63d486f48d
commit cc3d0a8b07
7 changed files with 1031 additions and 10 deletions
@@ -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
@@ -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"`
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"`
}
@@ -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)
}
@@ -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)
})
})
})
}
@@ -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
}
@@ -53,7 +53,4 @@ type FeedbackRepository interface {
// Search находит отзывы по тексту
Search(query string, offset, limit int) ([]models.Feedback, error)
// Добавьте в интерфейс FeedbackRepository:
}
@@ -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
}
}
}