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:
@@ -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"`
|
||||
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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -52,8 +52,5 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user