On branch main

modified:   main_dc/yalarba/api_yal/internal/domain/appeal/router.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/dto.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/handler.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/router.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/service.go
	modified:   main_dc/yalarba/api_yal/internal/models/feedback.go
	modified:   main_dc/yalarba/api_yal/internal/repository/comment_repository.go
	modified:   main_dc/yalarba/api_yal/internal/repository/feedback_repository.go
	modified:   main_dc/yalarba/api_yal/internal/repository/feedback_repository_impl.go
	modified:   main_dc/yalarba/api_yal/internal/router/router.go
craete routerRegister, service, hander, dto for feedback
This commit is contained in:
2026-05-19 15:01:57 +05:00
parent 42549eb116
commit 63d486f48d
10 changed files with 756 additions and 1156 deletions
@@ -1,9 +1,3 @@
package appeal package appeal
import () import ()
func NewHandler(service AppealService) *AppealHandler {
return &AppealHandler{
service: service,
}
}
@@ -1,88 +1,107 @@
package feetback package feetback
import ( import (
"api_yal/internal/domain/account"
"api_yal/internal/domain/comment"
"api_yal/internal/domain/object"
"api_yal/internal/models" "api_yal/internal/models"
"time" "time"
) )
// CreateFeedbackRequest - DTO для создания отзыва // CreateFeedbackRequest DTO для создания отзыва
// Обязательные поля: ObjectID, Platform, Score, Text
// Score должен быть от 1 до 5
// Platform должен быть одним из допустимых значений (entrepreneur, tourist)
type CreateFeedbackRequest struct { type CreateFeedbackRequest struct {
ObjectID uint `json:"object_id" binding:"required"` ObjectID uint `json:"object_id" binding:"required"`
Platform models.PlatformType `json:"platform" binding:"required,oneof=entrepreneur tourist"` Rating int `json:"rating" binding:"required,min=1,max=5"`
Score int `json:"score" binding:"required,min=1,max=5"` Text string `json:"text" binding:"required,min=1,max=2000"`
Text string `json:"text" binding:"required"` Platform models.PlatformType `json:"platform" binding:"required"`
MediaURLs []string `json:"media_urls"`
} }
// UpdateFeedbackRequest - DTO для обновления отзыва // UpdateFeedbackRequest DTO для обновления отзыва
// Все поля опциональны, позволяют обновлять только указанные данные
type UpdateFeedbackRequest struct { type UpdateFeedbackRequest struct {
Score *int `json:"score" binding:"omitempty,min=1,max=5"` Rating *int `json:"rating,omitempty" binding:"omitempty,min=1,max=5"`
Text *string `json:"text"` Text *string `json:"text,omitempty" binding:"omitempty,min=1,max=2000"`
Platform *models.PlatformType `json:"platform,omitempty"`
MediaURLs []string `json:"media_urls"`
} }
// FeedbackResponse - DTO для полного ответа с отзывом // FeedbackResponse DTO для ответа
// Включает всю информацию о отзыве, включая связанные данные
// Owner и Object могут быть nil, если предзагрузка не была выполнена
type FeedbackResponse struct { type FeedbackResponse struct {
ID uint `json:"id"` ID uint `json:"id"`
CreatedAt time.Time `json:"created_at"` OwnerID uint `json:"owner_id"`
UpdatedAt time.Time `json:"updated_at"` Owner *AccountBriefResponse `json:"owner,omitempty"`
DeletedAt *time.Time `json:"deleted_at,omitempty"` ObjectID uint `json:"object_id"`
OwnerID uint `json:"owner_id"` Object *ObjectBriefResponse `json:"object,omitempty"`
Owner *account.AccountResponse `json:"owner,omitempty"` Rating int `json:"rating"`
ObjectID uint `json:"object_id"` Text string `json:"text"`
Object *object.ObjectShortResponse `json:"object,omitempty"` Platform models.PlatformType `json:"platform"`
Platform models.PlatformType `json:"platform"` MediaURLs []string `json:"media_urls"`
Score int `json:"score"` CommentCount int `json:"comment_count"`
Text string `json:"text"` CreatedAt time.Time `json:"created_at"`
CommentCount int `json:"comment_count"` UpdatedAt time.Time `json:"updated_at"`
Comments []comment.CommentShortResponse `json:"comments,omitempty"`
} }
// FeedbackShortResponse - DTO для краткого ответа (вложенный в объект) // AccountBriefResponse краткая информация о пользователе
// Используется при возврате отзывов в составе других объектов type AccountBriefResponse struct {
// OwnerName берется из Account.OwnerName или формируется из имени пользователя ID uint `json:"id"`
type FeedbackShortResponse struct { Username string `json:"username"`
ID uint `json:"id"` AvatarURL string `json:"avatar_url"`
CreatedAt time.Time `json:"created_at"`
OwnerID uint `json:"owner_id"`
OwnerName string `json:"owner_name,omitempty"`
Platform models.PlatformType `json:"platform"`
Score int `json:"score"`
Text string `json:"text"`
} }
// FeedbackListResponse - DTO для списка отзывов с пагинацией // ObjectBriefResponse краткая информация об объекте
// Структура для возврата списка отзывов с метаданными пагинации type ObjectBriefResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}
// FeedbackListResponse ответ со списком отзывов
type FeedbackListResponse struct { type FeedbackListResponse struct {
Items []FeedbackShortResponse `json:"items"` Data []FeedbackResponse `json:"data"`
Total int64 `json:"total"` Total int64 `json:"total"`
Page int `json:"page"` Offset int `json:"offset"`
PageSize int `json:"page_size"` Limit int `json:"limit"`
TotalPages int `json:"total_pages"` NextOffset *int `json:"next_offset,omitempty"`
} }
// SearchFeedbacksRequest - DTO для поиска отзывов // CreateCommentRequest DTO для создания комментария
// Используется для параметров поиска type CreateCommentRequest struct {
// Запрос должен быть не пустым Text string `json:"text" binding:"required,min=1,max=1000"`
type SearchFeedbacksRequest struct {
Query string `json:"q" binding:"required"`
Page int `json:"page"`
PageSize int `json:"page_size"`
} }
// FeedbackStatsResponse - DTO для статистики по отзывам // UpdateCommentRequest DTO для обновления комментария
// Используется для агрегированной информации type UpdateCommentRequest struct {
// AverageScore может быть 0, если нет отзывов Text string `json:"text" binding:"required,min=1,max=1000"`
}
// CommentResponse DTO для ответа с комментарием
type CommentResponse struct {
ID uint `json:"id"`
FeedbackID uint `json:"feedback_id"`
AccountID uint `json:"account_id"`
Account *AccountBriefResponse `json:"account,omitempty"`
Text string `json:"text"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CommentListResponse ответ со списком комментариев
type CommentListResponse struct {
Data []CommentResponse `json:"data"`
Total int `json:"total"`
Offset int `json:"offset"`
Limit int `json:"limit"`
NextOffset *int `json:"next_offset,omitempty"`
}
// FeedbackStatsResponse статистика по отзывам
type FeedbackStatsResponse struct { type FeedbackStatsResponse struct {
TotalCount int `json:"total_count"` TotalFeedbacks int64 `json:"total_feedbacks"`
AverageScore float64 `json:"average_score"` AverageRating float64 `json:"average_rating"`
PlatformStats map[models.PlatformType]int `json:"platform_stats"` RatingDistribution map[int]int64 `json:"rating_distribution"`
ScoreDistribution map[int]int `json:"score_distribution"` // распределение по баллам (1-5) PlatformStats map[models.PlatformType]int64 `json:"platform_stats"`
}
// ErrorResponse ответ с ошибкой
type ErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
Code int `json:"code"`
} }
@@ -1,388 +1,547 @@
package feetback package feetback
import ( import (
"api_yal/internal/models"
"api_yal/internal/logger"
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv" "strconv"
"api_yal/internal/logger"
"api_yal/internal/middleware"
"api_yal/internal/models"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"go.uber.org/zap" "go.uber.org/zap"
) )
// FeedbackHandler обработчик для отзывов
type FeedbackHandler struct { type FeedbackHandler struct {
service FeedbackService service FeedbackService
log *zap.Logger
} }
// NewFeedbackHandler создает новый обработчик
func NewFeedbackHandler(service FeedbackService) *FeedbackHandler { func NewFeedbackHandler(service FeedbackService) *FeedbackHandler {
return &FeedbackHandler{ return &FeedbackHandler{
service: service, service: service,
log: logger.Get(),
} }
} }
// getUserIDFromContext получает ID пользователя из контекста
func (h *FeedbackHandler) getUserIDFromContext(r *http.Request) (uint, bool) {
userID, ok := r.Context().Value("user_id").(uint)
return userID, ok
}
// isAdminFromContext проверяет, является ли пользователь админом
func (h *FeedbackHandler) isAdminFromContext(r *http.Request) bool {
isAdmin, ok := r.Context().Value("is_admin").(bool)
return ok && isAdmin
}
// parsePagination парсит параметры пагинации
func (h *FeedbackHandler) parsePagination(r *http.Request) (offset, limit int) {
offset, _ = strconv.Atoi(r.URL.Query().Get("offset"))
limit, _ = strconv.Atoi(r.URL.Query().Get("limit"))
if limit <= 0 || limit > 100 {
limit = 20
}
if offset < 0 {
offset = 0
}
return
}
// sendJSON отправляет JSON ответ
func (h *FeedbackHandler) sendJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
h.log.Error("Failed to encode JSON response", zap.Error(err))
}
}
// sendError отправляет ошибку
func (h *FeedbackHandler) sendError(w http.ResponseWriter, status int, message string) {
h.sendJSON(w, status, ErrorResponse{
Error: http.StatusText(status),
Message: message,
Code: status,
})
}
// CreateFeedback создает новый отзыв // CreateFeedback создает новый отзыв
// POST /feedbacks
func (h *FeedbackHandler) CreateFeedback(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) CreateFeedback(w http.ResponseWriter, r *http.Request) {
userID, ok := h.getUserIDFromContext(r)
if !ok {
h.sendError(w, http.StatusUnauthorized, "unauthorized")
return
}
var req CreateFeedbackRequest var req CreateFeedbackRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid request body")
return return
} }
response, err := h.service.Create(r.Context(), &req) feedback := &models.Feedback{
if err != nil { ObjectID: req.ObjectID,
logger.Get().Error("Failed to create feedback", zap.Error(err)) Score: req.Rating,
http.Error(w, err.Error(), http.StatusBadRequest) Text: req.Text,
Platform: req.Platform,
}
if err := h.service.Create(feedback, userID); err != nil {
h.log.Error("Failed to create feedback", zap.Error(err))
h.sendError(w, http.StatusInternalServerError, "failed to create feedback")
return return
} }
w.Header().Set("Content-Type", "application/json") // Загружаем созданный отзыв с данными владельца и объекта
w.WriteHeader(http.StatusCreated) created, _ := h.service.GetByID(feedback.ID)
json.NewEncoder(w).Encode(response) response := h.mapFeedbackToResponse(created)
h.sendJSON(w, http.StatusCreated, response)
} }
// GetFeedbackByID возвращает отзыв по ID // GetFeedbackByID получает отзыв по ID
// GET /feedbacks/{id}
func (h *FeedbackHandler) GetFeedbackByID(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) GetFeedbackByID(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id") idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil { if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid feedback id")
return return
} }
response, err := h.service.GetByID(r.Context(), uint(id)) feedback, err := h.service.GetByID(uint(id))
if err != nil { if err != nil {
logger.Get().Error("Failed to get feedback", zap.Error(err)) h.sendError(w, http.StatusNotFound, "feedback not found")
http.Error(w, err.Error(), http.StatusNotFound)
return return
} }
w.Header().Set("Content-Type", "application/json") response := h.mapFeedbackToResponse(feedback)
json.NewEncoder(w).Encode(response) h.sendJSON(w, http.StatusOK, response)
} }
// UpdateFeedback обновляет существующий отзыв // UpdateFeedback обновляет отзыв
// PUT /feedbacks/{id}
func (h *FeedbackHandler) UpdateFeedback(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) UpdateFeedback(w http.ResponseWriter, r *http.Request) {
userID, ok := h.getUserIDFromContext(r)
if !ok {
h.sendError(w, http.StatusUnauthorized, "unauthorized")
return
}
idStr := chi.URLParam(r, "id") idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil { if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid feedback id")
return return
} }
var req UpdateFeedbackRequest var req UpdateFeedbackRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid request body")
return return
} }
response, err := h.service.Update(r.Context(), uint(id), &req) updates := make(map[string]interface{})
if err != nil { if req.Rating != nil {
logger.Get().Error("Failed to update feedback", zap.Error(err)) updates["rating"] = *req.Rating
http.Error(w, err.Error(), http.StatusBadRequest) }
if req.Text != nil {
updates["text"] = *req.Text
}
if req.Platform != nil {
updates["platform"] = *req.Platform
}
if req.MediaURLs != nil {
updates["media_urls"] = req.MediaURLs
}
if err := h.service.Update(uint(id), userID, updates); err != nil {
if err.Error() == "you can only update your own feedback" {
h.sendError(w, http.StatusForbidden, err.Error())
return
}
h.log.Error("Failed to update feedback", zap.Error(err))
h.sendError(w, http.StatusInternalServerError, "failed to update feedback")
return return
} }
w.Header().Set("Content-Type", "application/json") feedback, _ := h.service.GetByID(uint(id))
json.NewEncoder(w).Encode(response) response := h.mapFeedbackToResponse(feedback)
h.sendJSON(w, http.StatusOK, response)
} }
// DeleteFeedback удаляет отзыв // DeleteFeedback удаляет отзыв
// DELETE /feedbacks/{id}
func (h *FeedbackHandler) DeleteFeedback(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) DeleteFeedback(w http.ResponseWriter, r *http.Request) {
userID, ok := h.getUserIDFromContext(r)
if !ok {
h.sendError(w, http.StatusUnauthorized, "unauthorized")
return
}
idStr := chi.URLParam(r, "id") idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil { if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid feedback id")
return return
} }
if err := h.service.Delete(r.Context(), uint(id)); err != nil { isAdmin := h.isAdminFromContext(r)
logger.Get().Error("Failed to delete feedback", zap.Error(err))
http.Error(w, err.Error(), http.StatusBadRequest) if err := h.service.Delete(uint(id), userID, isAdmin); err != nil {
if err.Error() == "you can only delete your own feedback" {
h.sendError(w, http.StatusForbidden, err.Error())
return
}
h.log.Error("Failed to delete feedback", zap.Error(err))
h.sendError(w, http.StatusInternalServerError, "failed to delete feedback")
return return
} }
w.WriteHeader(http.StatusNoContent) h.sendJSON(w, http.StatusNoContent, nil)
} }
// ListFeedbacks возвращает список отзывов с пагинацией // ListFeedbacks возвращает список отзывов с пагинацией
// GET /feedbacks
func (h *FeedbackHandler) ListFeedbacks(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) ListFeedbacks(w http.ResponseWriter, r *http.Request) {
page, _ := strconv.Atoi(r.URL.Query().Get("page")) offset, limit := h.parsePagination(r)
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
if page <= 0 { feedbacks, total, err := h.service.List(offset, limit)
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
response, err := h.service.List(r.Context(), page, pageSize)
if err != nil { if err != nil {
logger.Get().Error("Failed to list feedbacks", zap.Error(err)) h.log.Error("Failed to list feedbacks", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks")
return return
} }
w.Header().Set("Content-Type", "application/json") response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit)
json.NewEncoder(w).Encode(response) h.sendJSON(w, http.StatusOK, response)
} }
// GetMyFeedbacks возвращает отзывы текущего пользователя // GetMyFeedbacks возвращает отзывы текущего пользователя
// GET /feedbacks/my
func (h *FeedbackHandler) GetMyFeedbacks(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) GetMyFeedbacks(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserID(r.Context()) userID, ok := h.getUserIDFromContext(r)
if !ok { if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized) h.sendError(w, http.StatusUnauthorized, "unauthorized")
return return
} }
page, _ := strconv.Atoi(r.URL.Query().Get("page")) offset, limit := h.parsePagination(r)
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
if page <= 0 { feedbacks, total, err := h.service.ListByOwner(userID, offset, limit)
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
response, err := h.service.ListByOwner(r.Context(), userID, page, pageSize)
if err != nil { if err != nil {
logger.Get().Error("Failed to get user feedbacks", zap.Error(err)) h.log.Error("Failed to list user feedbacks", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks")
return return
} }
w.Header().Set("Content-Type", "application/json") response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit)
json.NewEncoder(w).Encode(response) h.sendJSON(w, http.StatusOK, response)
} }
// GetFeedbacksByObject возвращает отзывы по объекту // GetFeedbacksByObject возвращает отзывы по объекту
// GET /feedbacks/object/{objectID}
func (h *FeedbackHandler) GetFeedbacksByObject(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) GetFeedbacksByObject(w http.ResponseWriter, r *http.Request) {
objectIDStr := chi.URLParam(r, "objectID") objectIDStr := chi.URLParam(r, "objectID")
objectID, err := strconv.ParseUint(objectIDStr, 10, 32) objectID, err := strconv.ParseUint(objectIDStr, 10, 32)
if err != nil { if err != nil {
http.Error(w, "Invalid object ID", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid object id")
return return
} }
page, _ := strconv.Atoi(r.URL.Query().Get("page")) offset, limit := h.parsePagination(r)
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
if page <= 0 { feedbacks, total, err := h.service.ListByObject(uint(objectID), offset, limit)
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
response, err := h.service.ListByObject(r.Context(), uint(objectID), page, pageSize)
if err != nil { if err != nil {
logger.Get().Error("Failed to get object feedbacks", zap.Error(err)) h.log.Error("Failed to list feedbacks by object", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks")
return return
} }
w.Header().Set("Content-Type", "application/json") response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit)
json.NewEncoder(w).Encode(response) h.sendJSON(w, http.StatusOK, response)
} }
// GetFeedbacksByPlatform возвращает отзывы по платформе // GetFeedbacksByPlatform возвращает отзывы по платформе
// GET /feedbacks/platform/{platform}
func (h *FeedbackHandler) GetFeedbacksByPlatform(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) GetFeedbacksByPlatform(w http.ResponseWriter, r *http.Request) {
platformStr := chi.URLParam(r, "platform") platformStr := chi.URLParam(r, "platform")
platform := models.PlatformType(platformStr) platform := models.PlatformType(platformStr)
page, _ := strconv.Atoi(r.URL.Query().Get("page")) offset, limit := h.parsePagination(r)
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
if page <= 0 { feedbacks, total, err := h.service.ListByPlatform(platform, offset, limit)
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
response, err := h.service.ListByPlatform(r.Context(), platform, page, pageSize)
if err != nil { if err != nil {
logger.Get().Error("Failed to get platform feedbacks", zap.Error(err)) h.log.Error("Failed to list feedbacks by platform", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks")
return return
} }
w.Header().Set("Content-Type", "application/json") response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit)
json.NewEncoder(w).Encode(response) h.sendJSON(w, http.StatusOK, response)
} }
// SearchFeedbacks ищет отзывы по тексту // SearchFeedbacks ищет отзывы по тексту
// GET /feedbacks/search
func (h *FeedbackHandler) SearchFeedbacks(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) SearchFeedbacks(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q") query := r.URL.Query().Get("q")
if query == "" { if query == "" {
http.Error(w, "Search query is required", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "search query is required")
return return
} }
page, _ := strconv.Atoi(r.URL.Query().Get("page")) offset, limit := h.parsePagination(r)
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
if page <= 0 { feedbacks, total, err := h.service.Search(query, offset, limit)
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
response, err := h.service.Search(r.Context(), query, page, pageSize)
if err != nil { if err != nil {
logger.Get().Error("Failed to search feedbacks", zap.Error(err)) h.log.Error("Failed to search feedbacks", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) h.sendError(w, http.StatusInternalServerError, "failed to search feedbacks")
return return
} }
w.Header().Set("Content-Type", "application/json") response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit)
json.NewEncoder(w).Encode(response) h.sendJSON(w, http.StatusOK, response)
}
// GetFeedbackStats возвращает статистику по отзывам
func (h *FeedbackHandler) GetFeedbackStats(w http.ResponseWriter, r *http.Request) {
stats, err := h.service.GetStats(r.Context())
if err != nil {
logger.Get().Error("Failed to get feedback stats", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(stats)
} }
// GetFeedbackComments возвращает комментарии к отзыву // GetFeedbackComments возвращает комментарии к отзыву
// GET /feedbacks/{id}/comments
func (h *FeedbackHandler) GetFeedbackComments(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) GetFeedbackComments(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id") idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil { if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid feedback id")
return return
} }
page, _ := strconv.Atoi(r.URL.Query().Get("page")) offset, limit := h.parsePagination(r)
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
if page <= 0 { comments, total, err := h.service.GetComments(uint(id), offset, limit)
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
comments, total, err := h.service.GetComments(r.Context(), uint(id), page, pageSize)
if err != nil { if err != nil {
logger.Get().Error("Failed to get comments", zap.Error(err)) h.log.Error("Failed to get comments", zap.Error(err))
http.Error(w, err.Error(), http.StatusInternalServerError) h.sendError(w, http.StatusInternalServerError, "failed to get comments")
return return
} }
response := map[string]interface{}{ response := h.mapCommentListToResponse(comments, total, offset, limit)
"items": comments, h.sendJSON(w, http.StatusOK, response)
"total": total,
"page": page,
"page_size": pageSize,
"total_pages": int((total + int64(pageSize) - 1) / int64(pageSize)),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
} }
// AddComment добавляет комментарий к отзыву // AddComment добавляет комментарий к отзыву
// POST /feedbacks/{id}/comments
func (h *FeedbackHandler) AddComment(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) AddComment(w http.ResponseWriter, r *http.Request) {
userID, ok := h.getUserIDFromContext(r)
if !ok {
h.sendError(w, http.StatusUnauthorized, "unauthorized")
return
}
idStr := chi.URLParam(r, "id") idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32) id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil { if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid feedback id")
return return
} }
var req struct { var req CreateCommentRequest
Text string `json:"text" binding:"required"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid request body")
return return
} }
comment, err := h.service.AddComment(r.Context(), uint(id), req.Text) comment, err := h.service.AddComment(uint(id), userID, req.Text)
if err != nil { if err != nil {
logger.Get().Error("Failed to add comment", zap.Error(err)) if err.Error() == "feedback not found" {
http.Error(w, err.Error(), http.StatusBadRequest) h.sendError(w, http.StatusNotFound, err.Error())
return
}
h.log.Error("Failed to add comment", zap.Error(err))
h.sendError(w, http.StatusInternalServerError, "failed to add comment")
return return
} }
w.Header().Set("Content-Type", "application/json") response := h.mapCommentToResponse(comment)
w.WriteHeader(http.StatusCreated) h.sendJSON(w, http.StatusCreated, response)
json.NewEncoder(w).Encode(comment)
} }
// UpdateComment обновляет комментарий // UpdateComment обновляет комментарий
// PUT /feedbacks/{id}/comments/{commentID}
func (h *FeedbackHandler) UpdateComment(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) UpdateComment(w http.ResponseWriter, r *http.Request) {
userID, ok := h.getUserIDFromContext(r)
if !ok {
h.sendError(w, http.StatusUnauthorized, "unauthorized")
return
}
commentIDStr := chi.URLParam(r, "commentID") commentIDStr := chi.URLParam(r, "commentID")
commentID, err := strconv.ParseUint(commentIDStr, 10, 32) commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
if err != nil { if err != nil {
http.Error(w, "Invalid comment ID", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid comment id")
return return
} }
var req struct { var req UpdateCommentRequest
Text string `json:"text" binding:"required"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid request body")
return return
} }
if err := h.service.UpdateComment(r.Context(), uint(commentID), req.Text); err != nil { isAdmin := h.isAdminFromContext(r)
logger.Get().Error("Failed to update comment", zap.Error(err))
http.Error(w, err.Error(), http.StatusBadRequest) if err := h.service.UpdateComment(uint(commentID), userID, req.Text, isAdmin); err != nil {
if err.Error() == "you can only update your own comments" {
h.sendError(w, http.StatusForbidden, err.Error())
return
}
h.log.Error("Failed to update comment", zap.Error(err))
h.sendError(w, http.StatusInternalServerError, "failed to update comment")
return return
} }
w.WriteHeader(http.StatusOK) h.sendJSON(w, http.StatusOK, map[string]string{"message": "comment updated successfully"})
json.NewEncoder(w).Encode(map[string]string{"message": "Comment updated successfully"})
} }
// DeleteComment удаляет комментарий // DeleteComment удаляет комментарий
// DELETE /feedbacks/{id}/comments/{commentID}
func (h *FeedbackHandler) DeleteComment(w http.ResponseWriter, r *http.Request) { func (h *FeedbackHandler) DeleteComment(w http.ResponseWriter, r *http.Request) {
userID, ok := h.getUserIDFromContext(r)
if !ok {
h.sendError(w, http.StatusUnauthorized, "unauthorized")
return
}
commentIDStr := chi.URLParam(r, "commentID") commentIDStr := chi.URLParam(r, "commentID")
commentID, err := strconv.ParseUint(commentIDStr, 10, 32) commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
if err != nil { if err != nil {
http.Error(w, "Invalid comment ID", http.StatusBadRequest) h.sendError(w, http.StatusBadRequest, "invalid comment id")
return return
} }
if err := h.service.DeleteComment(r.Context(), uint(commentID)); err != nil { isAdmin := h.isAdminFromContext(r)
logger.Get().Error("Failed to delete comment", zap.Error(err))
http.Error(w, err.Error(), http.StatusBadRequest) if err := h.service.DeleteComment(uint(commentID), userID, isAdmin); err != nil {
if err.Error() == "you can only delete your own comments" {
h.sendError(w, http.StatusForbidden, err.Error())
return
}
h.log.Error("Failed to delete comment", zap.Error(err))
h.sendError(w, http.StatusInternalServerError, "failed to delete comment")
return return
} }
w.WriteHeader(http.StatusNoContent) h.sendJSON(w, http.StatusNoContent, nil)
}
// GetFeedbackStats возвращает статистику по отзывам
// GET /feedbacks/stats
func (h *FeedbackHandler) GetFeedbackStats(w http.ResponseWriter, r *http.Request) {
stats, err := h.service.GetStats()
if err != nil {
h.log.Error("Failed to get stats", zap.Error(err))
h.sendError(w, http.StatusInternalServerError, "failed to get stats")
return
}
h.sendJSON(w, http.StatusOK, stats)
}
// mapFeedbackToResponse маппинг модели в DTO ответа
func (h *FeedbackHandler) mapFeedbackToResponse(f *models.Feedback) FeedbackResponse {
resp := FeedbackResponse{
ID: f.ID,
OwnerID: f.OwnerID,
ObjectID: f.ObjectID,
Rating: f.Score,
Text: f.Text,
Platform: f.Platform,
CommentCount: f.CommentCount,
CreatedAt: f.CreatedAt,
UpdatedAt: f.UpdatedAt,
}
if f.OwnerID != 0 {
resp.Owner = &AccountBriefResponse{
ID: f.Owner.ID,
Username: f.Owner.FullName,
}
}
if f.ObjectID != 0 {
resp.Object = &ObjectBriefResponse{
ID: f.Object.ID,
Name: f.Object.Owner.FullName,
Description: f.Object.ShortDescription,
}
}
return resp
}
// mapFeedbackListToResponse маппинг списка отзывов в DTO ответа
func (h *FeedbackHandler) mapFeedbackListToResponse(feedbacks []models.Feedback, total int64, offset, limit int) FeedbackListResponse {
data := make([]FeedbackResponse, len(feedbacks))
for i, f := range feedbacks {
data[i] = h.mapFeedbackToResponse(&f)
}
resp := FeedbackListResponse{
Data: data,
Total: total,
Offset: offset,
Limit: limit,
}
nextOffset := offset + limit
if int64(nextOffset) < total {
resp.NextOffset = &nextOffset
}
return resp
}
// mapCommentToResponse маппинг комментария в DTO ответа
func (h *FeedbackHandler) mapCommentToResponse(c *models.Comment) CommentResponse {
resp := CommentResponse{
ID: c.ID,
FeedbackID: c.FeedbackID,
AccountID: c.AuthorID,
Text: c.Text,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
if c.AuthorID != 0 {
resp.Account = &AccountBriefResponse{
ID: c.Author.ID,
Username: c.Author.FullName,
}
}
return resp
}
// mapCommentListToResponse маппинг списка комментариев в DTO ответа
func (h *FeedbackHandler) mapCommentListToResponse(comments []models.Comment, total, offset, limit int) CommentListResponse {
data := make([]CommentResponse, len(comments))
for i, c := range comments {
data[i] = h.mapCommentToResponse(&c)
}
resp := CommentListResponse{
Data: data,
Total: total,
Offset: offset,
Limit: limit,
}
nextOffset := offset + limit
if nextOffset < total {
resp.NextOffset = &nextOffset
}
return resp
} }
@@ -14,39 +14,33 @@ func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) {
l.Info("Registering routes for feetback") l.Info("Registering routes for feetback")
feedbackRepo := repository.NewFeedbackRepository(db) feedbackRepo := repository.NewFeedbackRepository(db)
feedbackService := NewFeedbackServiceImpl(feedbackRepo) feedbackService := NewFeedbackServiceImpl(feedbackRepo, db)
feedbackHandler := NewFeedbackHandler(feedbackService) feedbackHandler := NewFeedbackHandler(feedbackService)
// Группируем маршруты для отзывов // Группируем маршруты для отзывов
r.Route("/feedbacks", func(r chi.Router) { r.Route("/feedbacks", func(r chi.Router) {
// Публичные маршруты (не требуют аутентификации) // Публичные маршруты (не требуют аутентификации)
r.Get("/", feedbackHandler.ListFeedbacks) // GET /api/v1/feedbacks r.Get("/", feedbackHandler.ListFeedbacks)
r.Get("/search", feedbackHandler.SearchFeedbacks) // GET /api/v1/feedbacks/search?q=query r.Get("/search", feedbackHandler.SearchFeedbacks)
r.Get("/{id}", feedbackHandler.GetFeedbackByID) // GET /api/v1/feedbacks/{id} r.Get("/stats", feedbackHandler.GetFeedbackStats) // Новый эндпоинт
r.Get("/{id}", feedbackHandler.GetFeedbackByID)
// Маршруты для фильтрации r.Get("/object/{objectID}", feedbackHandler.GetFeedbacksByObject)
r.Get("/object/{objectID}", feedbackHandler.GetFeedbacksByObject) // GET /api/v1/feedbacks/object/{objectID} r.Get("/platform/{platform}", feedbackHandler.GetFeedbacksByPlatform)
r.Get("/platform/{platform}", feedbackHandler.GetFeedbacksByPlatform) // GET /api/v1/feedbacks/platform/{platform} r.Get("/{id}/comments", feedbackHandler.GetFeedbackComments)
// Маршруты для комментариев (публичные)
r.Get("/{id}/comments", feedbackHandler.GetFeedbackComments) // GET /api/v1/feedbacks/{id}/comments
// Защищенные маршруты (требуют аутентификации) // Защищенные маршруты (требуют аутентификации)
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
// Здесь можно добавить middleware для проверки JWT токена
r.Use(middleware.AuthMiddleware(jwtSecret)) r.Use(middleware.AuthMiddleware(jwtSecret))
r.Post("/", feedbackHandler.CreateFeedback) // POST /api/v1/feedbacks r.Post("/", feedbackHandler.CreateFeedback)
r.Put("/{id}", feedbackHandler.UpdateFeedback) // PUT /api/v1/feedbacks/{id} r.Put("/{id}", feedbackHandler.UpdateFeedback)
r.Delete("/{id}", feedbackHandler.DeleteFeedback) // DELETE /api/v1/feedbacks/{id} r.Delete("/{id}", feedbackHandler.DeleteFeedback)
r.Get("/my", feedbackHandler.GetMyFeedbacks)
// Маршруты для комментариев (требуют аутентификации) // Маршруты для комментариев
r.Post("/{id}/comments", feedbackHandler.AddComment) // POST /api/v1/feedbacks/{id}/comments r.Post("/{id}/comments", feedbackHandler.AddComment)
r.Put("/{id}/comments/{commentID}", feedbackHandler.UpdateComment) // PUT /api/v1/feedbacks/{id}/comments/{commentID} r.Put("/{id}/comments/{commentID}", feedbackHandler.UpdateComment)
r.Delete("/{id}/comments/{commentID}", feedbackHandler.DeleteComment) // DELETE /api/v1/feedbacks/{id}/comments/{commentID} r.Delete("/{id}/comments/{commentID}", feedbackHandler.DeleteComment)
// Маршруты для владельца (получение своих отзывов)
r.Get("/my", feedbackHandler.GetMyFeedbacks) // GET /api/v1/feedbacks/my
}) })
}) })
} }
File diff suppressed because it is too large Load Diff
@@ -8,11 +8,11 @@ type Feedback struct {
// owner account ID // owner account ID
OwnerID uint `gorm:"not null;index" json:"owner_id"` OwnerID uint `gorm:"not null;index" json:"owner_id"`
Owner Account `gorm:"foreignKey:OwnerID;references:ID" json:"owner,omitempty"` Owner Account `gorm:"foreignKey:OwnerID;references:ID" json:"owner"`
// object IO // object IO
ObjectID uint `gorm:"not null;index" json:"object_id"` ObjectID uint `gorm:"not null;index" json:"object_id"`
Object Object `gorm:"foreignKey:ObjectID;references:ID" json:"object,omitempty"` Object Object `gorm:"foreignKey:ObjectID;references:ID" json:"object"`
// enterprener / tourist // enterprener / tourist
Platform PlatformType `json:"platform"` Platform PlatformType `json:"platform"`
@@ -51,4 +51,6 @@ type CommentRepository interface {
// ToggleVerification переключает статус верификации комментария // ToggleVerification переключает статус верификации комментария
ToggleVerification(id uint, verified bool) error ToggleVerification(id uint, verified bool) error
} }
@@ -55,23 +55,5 @@ type FeedbackRepository interface {
// Добавьте в интерфейс FeedbackRepository: // Добавьте в интерфейс FeedbackRepository:
// Comment methods
CreateComment(comment *models.Comment) error
GetCommentByID(id uint) (*models.Comment, error)
UpdateComment(comment *models.Comment) error
DeleteComment(id uint) error
// Additional methods for stats
GetAverageScore() (float64, error)
GetPlatformStats() (map[models.PlatformType]int, error)
GetScoreDistribution() (map[int]int, error)
// Count methods
CountByOwner(ownerID uint) (int64, error)
CountByObject(objectID uint) (int64, error)
CountByPlatform(platform models.PlatformType) (int64, error)
CountBySearch(query string) (int64, error)
// GetObject by ID (general method)
GetObjectByID(id uint) (*models.Object, error)
} }
@@ -158,126 +158,3 @@ func (r *feedbackRepositoryImpl) getObjectByID(id uint) (*models.Object, error)
return &object, nil return &object, nil
} }
// CreateComment создает новый комментарий
func (r *feedbackRepositoryImpl) CreateComment(comment *models.Comment) error {
return r.db.Create(comment).Error
}
// GetCommentByID возвращает комментарий по ID
func (r *feedbackRepositoryImpl) GetCommentByID(id uint) (*models.Comment, error) {
var comment models.Comment
err := r.db.First(&comment, id).Error
if err != nil {
return nil, err
}
return &comment, nil
}
// UpdateComment обновляет комментарий
func (r *feedbackRepositoryImpl) UpdateComment(comment *models.Comment) error {
return r.db.Save(comment).Error
}
// DeleteComment удаляет комментарий
func (r *feedbackRepositoryImpl) DeleteComment(id uint) error {
return r.db.Delete(&models.Comment{}, id).Error
}
// GetAverageScore возвращает средний балл отзывов
func (r *feedbackRepositoryImpl) GetAverageScore() (float64, error) {
var avg float64
err := r.db.Model(&models.Feedback{}).Select("COALESCE(AVG(score), 0)").Scan(&avg).Error
return avg, err
}
// GetPlatformStats возвращает статистику по платформам
func (r *feedbackRepositoryImpl) GetPlatformStats() (map[models.PlatformType]int, error) {
type Result struct {
Platform models.PlatformType
Count int
}
var results []Result
err := r.db.Model(&models.Feedback{}).
Select("platform, COUNT(*) as count").
Group("platform").
Scan(&results).Error
if err != nil {
return nil, err
}
stats := make(map[models.PlatformType]int)
for _, r := range results {
stats[r.Platform] = r.Count
}
return stats, nil
}
// GetScoreDistribution возвращает распределение оценок
func (r *feedbackRepositoryImpl) GetScoreDistribution() (map[int]int, error) {
type Result struct {
Score int
Count int
}
var results []Result
err := r.db.Model(&models.Feedback{}).
Select("score, COUNT(*) as count").
Group("score").
Scan(&results).Error
if err != nil {
return nil, err
}
distribution := make(map[int]int)
for i := 1; i <= 5; i++ {
distribution[i] = 0
}
for _, r := range results {
distribution[r.Score] = r.Count
}
return distribution, nil
}
// CountByOwner возвращает количество отзывов владельца
func (r *feedbackRepositoryImpl) CountByOwner(ownerID uint) (int64, error) {
var count int64
err := r.db.Model(&models.Feedback{}).Where("owner_id = ?", ownerID).Count(&count).Error
return count, err
}
// CountByObject возвращает количество отзывов объекта
func (r *feedbackRepositoryImpl) CountByObject(objectID uint) (int64, error) {
var count int64
err := r.db.Model(&models.Feedback{}).Where("object_id = ?", objectID).Count(&count).Error
return count, err
}
// CountByPlatform возвращает количество отзывов платформы
func (r *feedbackRepositoryImpl) CountByPlatform(platform models.PlatformType) (int64, error) {
var count int64
err := r.db.Model(&models.Feedback{}).Where("platform = ?", platform).Count(&count).Error
return count, err
}
// CountBySearch возвращает количество отзывов по поисковому запросу
func (r *feedbackRepositoryImpl) CountBySearch(query string) (int64, error) {
var count int64
err := r.db.Model(&models.Feedback{}).Where("text LIKE ?", "%"+query+"%").Count(&count).Error
return count, err
}
// GetObjectByID возвращает объект по ID
func (r *feedbackRepositoryImpl) GetObjectByID(id uint) (*models.Object, error) {
var object models.Object
err := r.db.First(&object, id).Error
if err != nil {
return nil, err
}
return &object, nil
}
@@ -2,9 +2,10 @@ package router
import ( import (
"api_yal/internal/config" "api_yal/internal/config"
"api_yal/internal/logger"
"api_yal/internal/domain/auth"
"api_yal/internal/domain/account" "api_yal/internal/domain/account"
"api_yal/internal/domain/auth"
"api_yal/internal/domain/feetback"
"api_yal/internal/logger"
"time" "time"
"encoding/json" "encoding/json"
@@ -53,6 +54,10 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
// Регистрируем маршруты аккаунтов // Регистрируем маршруты аккаунтов
account.RegisterRoutes(r, db, config.JWTSecret) account.RegisterRoutes(r, db, config.JWTSecret)
// Регистрируем маршруты отзывов
feetback.RegisterRoutes(r, db, config.JWTSecret) // Добавьте эту строку
}) })
zapLogger.Info("Настройка маршрутов завершена") zapLogger.Info("Настройка маршрутов завершена")