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:
@@ -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"`
|
||||||
|
OwnerID uint `json:"owner_id"`
|
||||||
|
Owner *AccountBriefResponse `json:"owner,omitempty"`
|
||||||
|
ObjectID uint `json:"object_id"`
|
||||||
|
Object *ObjectBriefResponse `json:"object,omitempty"`
|
||||||
|
Rating int `json:"rating"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Platform models.PlatformType `json:"platform"`
|
||||||
|
MediaURLs []string `json:"media_urls"`
|
||||||
|
CommentCount int `json:"comment_count"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt *time.Time `json:"deleted_at,omitempty"`
|
|
||||||
OwnerID uint `json:"owner_id"`
|
|
||||||
Owner *account.AccountResponse `json:"owner,omitempty"`
|
|
||||||
ObjectID uint `json:"object_id"`
|
|
||||||
Object *object.ObjectShortResponse `json:"object,omitempty"`
|
|
||||||
Platform models.PlatformType `json:"platform"`
|
|
||||||
Score int `json:"score"`
|
|
||||||
Text string `json:"text"`
|
|
||||||
CommentCount int `json:"comment_count"`
|
|
||||||
Comments []comment.CommentShortResponse `json:"comments,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FeedbackShortResponse - DTO для краткого ответа (вложенный в объект)
|
// AccountBriefResponse краткая информация о пользователе
|
||||||
// Используется при возврате отзывов в составе других объектов
|
type AccountBriefResponse struct {
|
||||||
// OwnerName берется из Account.OwnerName или формируется из имени пользователя
|
|
||||||
type FeedbackShortResponse struct {
|
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
Username string `json:"username"`
|
||||||
OwnerID uint `json:"owner_id"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
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("Настройка маршрутов завершена")
|
||||||
|
|||||||
Reference in New Issue
Block a user