63d486f48d
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
547 lines
16 KiB
Go
547 lines
16 KiB
Go
package feetback
|
|
|
|
import (
|
|
"api_yal/internal/models"
|
|
"api_yal/internal/logger"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// FeedbackHandler обработчик для отзывов
|
|
type FeedbackHandler struct {
|
|
service FeedbackService
|
|
log *zap.Logger
|
|
}
|
|
|
|
// NewFeedbackHandler создает новый обработчик
|
|
func NewFeedbackHandler(service FeedbackService) *FeedbackHandler {
|
|
return &FeedbackHandler{
|
|
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 создает новый отзыв
|
|
// POST /feedbacks
|
|
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
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
feedback := &models.Feedback{
|
|
ObjectID: req.ObjectID,
|
|
Score: req.Rating,
|
|
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
|
|
}
|
|
|
|
// Загружаем созданный отзыв с данными владельца и объекта
|
|
created, _ := h.service.GetByID(feedback.ID)
|
|
response := h.mapFeedbackToResponse(created)
|
|
|
|
h.sendJSON(w, http.StatusCreated, response)
|
|
}
|
|
|
|
// GetFeedbackByID получает отзыв по ID
|
|
// GET /feedbacks/{id}
|
|
func (h *FeedbackHandler) GetFeedbackByID(w http.ResponseWriter, r *http.Request) {
|
|
idStr := chi.URLParam(r, "id")
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid feedback id")
|
|
return
|
|
}
|
|
|
|
feedback, err := h.service.GetByID(uint(id))
|
|
if err != nil {
|
|
h.sendError(w, http.StatusNotFound, "feedback not found")
|
|
return
|
|
}
|
|
|
|
response := h.mapFeedbackToResponse(feedback)
|
|
h.sendJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
// UpdateFeedback обновляет отзыв
|
|
// PUT /feedbacks/{id}
|
|
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")
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid feedback id")
|
|
return
|
|
}
|
|
|
|
var req UpdateFeedbackRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
updates := make(map[string]interface{})
|
|
if req.Rating != nil {
|
|
updates["rating"] = *req.Rating
|
|
}
|
|
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
|
|
}
|
|
|
|
feedback, _ := h.service.GetByID(uint(id))
|
|
response := h.mapFeedbackToResponse(feedback)
|
|
h.sendJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
// DeleteFeedback удаляет отзыв
|
|
// DELETE /feedbacks/{id}
|
|
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")
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid feedback id")
|
|
return
|
|
}
|
|
|
|
isAdmin := h.isAdminFromContext(r)
|
|
|
|
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
|
|
}
|
|
|
|
h.sendJSON(w, http.StatusNoContent, nil)
|
|
}
|
|
|
|
// ListFeedbacks возвращает список отзывов с пагинацией
|
|
// GET /feedbacks
|
|
func (h *FeedbackHandler) ListFeedbacks(w http.ResponseWriter, r *http.Request) {
|
|
offset, limit := h.parsePagination(r)
|
|
|
|
feedbacks, total, err := h.service.List(offset, limit)
|
|
if err != nil {
|
|
h.log.Error("Failed to list feedbacks", zap.Error(err))
|
|
h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks")
|
|
return
|
|
}
|
|
|
|
response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit)
|
|
h.sendJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
// GetMyFeedbacks возвращает отзывы текущего пользователя
|
|
// GET /feedbacks/my
|
|
func (h *FeedbackHandler) GetMyFeedbacks(w http.ResponseWriter, r *http.Request) {
|
|
userID, ok := h.getUserIDFromContext(r)
|
|
if !ok {
|
|
h.sendError(w, http.StatusUnauthorized, "unauthorized")
|
|
return
|
|
}
|
|
|
|
offset, limit := h.parsePagination(r)
|
|
|
|
feedbacks, total, err := h.service.ListByOwner(userID, offset, limit)
|
|
if err != nil {
|
|
h.log.Error("Failed to list user feedbacks", zap.Error(err))
|
|
h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks")
|
|
return
|
|
}
|
|
|
|
response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit)
|
|
h.sendJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
// GetFeedbacksByObject возвращает отзывы по объекту
|
|
// GET /feedbacks/object/{objectID}
|
|
func (h *FeedbackHandler) GetFeedbacksByObject(w http.ResponseWriter, r *http.Request) {
|
|
objectIDStr := chi.URLParam(r, "objectID")
|
|
objectID, err := strconv.ParseUint(objectIDStr, 10, 32)
|
|
if err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid object id")
|
|
return
|
|
}
|
|
|
|
offset, limit := h.parsePagination(r)
|
|
|
|
feedbacks, total, err := h.service.ListByObject(uint(objectID), offset, limit)
|
|
if err != nil {
|
|
h.log.Error("Failed to list feedbacks by object", zap.Error(err))
|
|
h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks")
|
|
return
|
|
}
|
|
|
|
response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit)
|
|
h.sendJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
// GetFeedbacksByPlatform возвращает отзывы по платформе
|
|
// GET /feedbacks/platform/{platform}
|
|
func (h *FeedbackHandler) GetFeedbacksByPlatform(w http.ResponseWriter, r *http.Request) {
|
|
platformStr := chi.URLParam(r, "platform")
|
|
platform := models.PlatformType(platformStr)
|
|
|
|
offset, limit := h.parsePagination(r)
|
|
|
|
feedbacks, total, err := h.service.ListByPlatform(platform, offset, limit)
|
|
if err != nil {
|
|
h.log.Error("Failed to list feedbacks by platform", zap.Error(err))
|
|
h.sendError(w, http.StatusInternalServerError, "failed to list feedbacks")
|
|
return
|
|
}
|
|
|
|
response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit)
|
|
h.sendJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
// SearchFeedbacks ищет отзывы по тексту
|
|
// GET /feedbacks/search
|
|
func (h *FeedbackHandler) SearchFeedbacks(w http.ResponseWriter, r *http.Request) {
|
|
query := r.URL.Query().Get("q")
|
|
if query == "" {
|
|
h.sendError(w, http.StatusBadRequest, "search query is required")
|
|
return
|
|
}
|
|
|
|
offset, limit := h.parsePagination(r)
|
|
|
|
feedbacks, total, err := h.service.Search(query, offset, limit)
|
|
if err != nil {
|
|
h.log.Error("Failed to search feedbacks", zap.Error(err))
|
|
h.sendError(w, http.StatusInternalServerError, "failed to search feedbacks")
|
|
return
|
|
}
|
|
|
|
response := h.mapFeedbackListToResponse(feedbacks, total, offset, limit)
|
|
h.sendJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
// GetFeedbackComments возвращает комментарии к отзыву
|
|
// GET /feedbacks/{id}/comments
|
|
func (h *FeedbackHandler) GetFeedbackComments(w http.ResponseWriter, r *http.Request) {
|
|
idStr := chi.URLParam(r, "id")
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid feedback id")
|
|
return
|
|
}
|
|
|
|
offset, limit := h.parsePagination(r)
|
|
|
|
comments, total, err := h.service.GetComments(uint(id), offset, limit)
|
|
if err != nil {
|
|
h.log.Error("Failed to get comments", zap.Error(err))
|
|
h.sendError(w, http.StatusInternalServerError, "failed to get comments")
|
|
return
|
|
}
|
|
|
|
response := h.mapCommentListToResponse(comments, total, offset, limit)
|
|
h.sendJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
// AddComment добавляет комментарий к отзыву
|
|
// POST /feedbacks/{id}/comments
|
|
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")
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid feedback id")
|
|
return
|
|
}
|
|
|
|
var req CreateCommentRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
comment, err := h.service.AddComment(uint(id), userID, req.Text)
|
|
if err != nil {
|
|
if err.Error() == "feedback not found" {
|
|
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
|
|
}
|
|
|
|
response := h.mapCommentToResponse(comment)
|
|
h.sendJSON(w, http.StatusCreated, response)
|
|
}
|
|
|
|
// UpdateComment обновляет комментарий
|
|
// PUT /feedbacks/{id}/comments/{commentID}
|
|
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")
|
|
commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
|
|
if err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid comment id")
|
|
return
|
|
}
|
|
|
|
var req UpdateCommentRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid request body")
|
|
return
|
|
}
|
|
|
|
isAdmin := h.isAdminFromContext(r)
|
|
|
|
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
|
|
}
|
|
|
|
h.sendJSON(w, http.StatusOK, map[string]string{"message": "comment updated successfully"})
|
|
}
|
|
|
|
// DeleteComment удаляет комментарий
|
|
// DELETE /feedbacks/{id}/comments/{commentID}
|
|
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")
|
|
commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
|
|
if err != nil {
|
|
h.sendError(w, http.StatusBadRequest, "invalid comment id")
|
|
return
|
|
}
|
|
|
|
isAdmin := h.isAdminFromContext(r)
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
} |