diff --git a/main_dc/yalarba/api_yal/internal/domain/rating/dto.go b/main_dc/yalarba/api_yal/internal/domain/rating/dto.go index 7253ef6..5d515b5 100644 --- a/main_dc/yalarba/api_yal/internal/domain/rating/dto.go +++ b/main_dc/yalarba/api_yal/internal/domain/rating/dto.go @@ -1,6 +1,87 @@ package rating import ( - + "api_yal/internal/models" + "time" ) +// CreateRatingRequest запрос на создание рейтинга +type CreateRatingRequest struct { + ObjectID uint `json:"object_id" validate:"required"` + Platform models.PlatformType `json:"platform" validate:"required,oneof=entrepreneur tourist"` +} + +// UpdateRatingRequest запрос на обновление рейтинга +type UpdateRatingRequest struct { + AverageScore *float64 `json:"average_score,omitempty"` + TotalVotes *int `json:"total_votes,omitempty"` +} + +// VoteRequest запрос на голосование +type VoteRequest struct { + Score int `json:"score" validate:"required,min=1,max=5"` +} + +// RatingResponse ответ с данными рейтинга +type RatingResponse struct { + ID uint `json:"id"` + OwnerID uint `json:"owner_id"` + ObjectID uint `json:"object_id"` + Platform models.PlatformType `json:"platform"` + AverageScore float64 `json:"average_score"` + TotalVotes int `json:"total_votes"` + VoteBreakdown *VoteBreakdownResponse `json:"vote_breakdown,omitempty"` + Object *ObjectBriefResponse `json:"object,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// VoteBreakdownResponse ответ с детализацией голосов +type VoteBreakdownResponse struct { + Score1 int `json:"score_1"` + Score2 int `json:"score_2"` + Score3 int `json:"score_3"` + Score4 int `json:"score_4"` + Score5 int `json:"score_5"` +} + +// ObjectBriefResponse краткая информация об объекте +type ObjectBriefResponse struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +// RatingVoteResponse ответ с данными голоса +type RatingVoteResponse struct { + ID uint `json:"id"` + Platform models.PlatformType `json:"platform"` + TargetID uint `json:"target_id"` + VoterID uint `json:"voter_id"` + Score int `json:"score"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// ListRatingsRequest параметры для списка рейтингов +type ListRatingsRequest struct { + Page int `query:"page" default:"1"` + PageSize int `query:"page_size" default:"20"` + Platform string `query:"platform"` + OwnerID uint `query:"owner_id"` + ObjectID uint `query:"object_id"` +} + +// ListRatingsResponse ответ со списком рейтингов +type ListRatingsResponse struct { + Ratings []RatingResponse `json:"ratings"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + +// UserRatingInfoResponse информация о голосе пользователя +type UserRatingInfoResponse struct { + HasVoted bool `json:"has_voted"` + UserScore *int `json:"user_score,omitempty"` +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/rating/handler.go b/main_dc/yalarba/api_yal/internal/domain/rating/handler.go new file mode 100644 index 0000000..91b029d --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/rating/handler.go @@ -0,0 +1,463 @@ +package rating + +import ( + "encoding/json" + "net/http" + "strconv" + + "api_yal/internal/logger" + "api_yal/internal/middleware" + "api_yal/internal/models" + + "github.com/go-chi/chi/v5" + "go.uber.org/zap" +) + +type RatingHandler struct { + service RatingService +} + +// NewRatingHandler создает новый обработчик рейтингов +func NewRatingHandler(service RatingService) *RatingHandler { + return &RatingHandler{ + service: service, + } +} + +// CreateRating создает новый рейтинг +// @Summary Create a new rating +// @Tags ratings +// @Accept json +// @Produce json +// @Param request body CreateRatingRequest true "Rating data" +// @Success 201 {object} RatingResponse +// @Router /ratings [post] +func (h *RatingHandler) CreateRating(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + userID, ok := middleware.GetUserID(r.Context()) + if !ok { + l.Debug("Unauthorized: user ID not found in context") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + var req CreateRatingRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + l.Debug("Invalid request body", zap.Error(err)) + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + rating, err := h.service.CreateRating(r.Context(), userID, &req) + if err != nil { + l.Error("Failed to create rating", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(rating) +} + +// GetRatingByID возвращает рейтинг по ID +// @Summary Get rating by ID +// @Tags ratings +// @Produce json +// @Param id path int true "Rating ID" +// @Success 200 {object} RatingResponse +// @Router /ratings/{id} [get] +func (h *RatingHandler) GetRatingByID(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + http.Error(w, "Invalid rating ID", http.StatusBadRequest) + return + } + + rating, err := h.service.GetRatingByID(r.Context(), uint(id)) + if err != nil { + l.Error("Failed to get rating", zap.Error(err)) + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(rating) +} + +// GetRatingByObjectAndPlatform возвращает рейтинг объекта по платформе +// @Summary Get rating by object ID and platform +// @Tags ratings +// @Produce json +// @Param objectID path int true "Object ID" +// @Param platform path string true "Platform (entrepreneur/tourist)" +// @Success 200 {object} RatingResponse +// @Router /ratings/object/{objectID}/platform/{platform} [get] +func (h *RatingHandler) GetRatingByObjectAndPlatform(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + objectIDStr := chi.URLParam(r, "objectID") + objectID, err := strconv.ParseUint(objectIDStr, 10, 32) + if err != nil { + http.Error(w, "Invalid object ID", http.StatusBadRequest) + return + } + + platform := models.PlatformType(chi.URLParam(r, "platform")) + if platform != models.PlatformEntrepreneur && platform != models.PlatformTourist { + http.Error(w, "Invalid platform", http.StatusBadRequest) + return + } + + rating, err := h.service.GetRatingByObjectAndPlatform(r.Context(), uint(objectID), platform) + if err != nil { + l.Error("Failed to get rating", zap.Error(err)) + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(rating) +} + +// UpdateRating обновляет рейтинг +// @Summary Update rating +// @Tags ratings +// @Accept json +// @Produce json +// @Param id path int true "Rating ID" +// @Param request body UpdateRatingRequest true "Update data" +// @Success 200 {object} RatingResponse +// @Router /ratings/{id} [put] +func (h *RatingHandler) UpdateRating(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + http.Error(w, "Invalid rating ID", http.StatusBadRequest) + return + } + + var req UpdateRatingRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + l.Debug("Invalid request body", zap.Error(err)) + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + rating, err := h.service.UpdateRating(r.Context(), uint(id), &req) + if err != nil { + l.Error("Failed to update rating", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(rating) +} + +// DeleteRating удаляет рейтинг +// @Summary Delete rating +// @Tags ratings +// @Param id path int true "Rating ID" +// @Success 204 "No Content" +// @Router /ratings/{id} [delete] +func (h *RatingHandler) DeleteRating(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + idStr := chi.URLParam(r, "id") + id, err := strconv.ParseUint(idStr, 10, 32) + if err != nil { + http.Error(w, "Invalid rating ID", http.StatusBadRequest) + return + } + + if err := h.service.DeleteRating(r.Context(), uint(id)); err != nil { + l.Error("Failed to delete rating", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// ListRatings возвращает список рейтингов +// @Summary List ratings +// @Tags ratings +// @Produce json +// @Param page query int false "Page number" default(1) +// @Param page_size query int false "Page size" default(20) +// @Param platform query string false "Filter by platform" +// @Param owner_id query int false "Filter by owner ID" +// @Param object_id query int false "Filter by object ID" +// @Success 200 {object} ListRatingsResponse +// @Router /ratings [get] +func (h *RatingHandler) ListRatings(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + req := &ListRatingsRequest{} + + // Парсинг query параметров + if pageStr := r.URL.Query().Get("page"); pageStr != "" { + if page, err := strconv.Atoi(pageStr); err == nil && page > 0 { + req.Page = page + } + } + if pageSizeStr := r.URL.Query().Get("page_size"); pageSizeStr != "" { + if pageSize, err := strconv.Atoi(pageSizeStr); err == nil && pageSize > 0 { + req.PageSize = pageSize + } + } + req.Platform = r.URL.Query().Get("platform") + if ownerIDStr := r.URL.Query().Get("owner_id"); ownerIDStr != "" { + if ownerID, err := strconv.ParseUint(ownerIDStr, 10, 32); err == nil { + req.OwnerID = uint(ownerID) + } + } + if objectIDStr := r.URL.Query().Get("object_id"); objectIDStr != "" { + if objectID, err := strconv.ParseUint(objectIDStr, 10, 32); err == nil { + req.ObjectID = uint(objectID) + } + } + + var response *ListRatingsResponse + var err error + + if req.ObjectID > 0 { + // Получаем все рейтинги объекта + ratings, err := h.service.GetRatingsByObject(r.Context(), req.ObjectID) + if err != nil { + l.Error("Failed to get ratings by object", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + response = &ListRatingsResponse{ + Ratings: ratings, + Total: int64(len(ratings)), + Page: 1, + PageSize: len(ratings), + TotalPages: 1, + } + } else if req.OwnerID > 0 { + response, err = h.service.GetRatingsByOwner(r.Context(), req.OwnerID, req) + } else { + response, err = h.service.ListRatings(r.Context(), req) + } + + if err != nil { + l.Error("Failed to list ratings", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// Vote голосование за объект +// @Summary Vote for an object +// @Tags ratings +// @Accept json +// @Produce json +// @Param request body VoteRequest true "Vote data" +// @Param targetID path int true "Target object ID" +// @Param platform path string true "Platform" +// @Success 200 {object} RatingResponse +// @Router /ratings/{targetID}/vote/{platform} [post] +func (h *RatingHandler) Vote(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + userID, ok := middleware.GetUserID(r.Context()) + if !ok { + l.Debug("Unauthorized: user ID not found in context") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + targetIDStr := chi.URLParam(r, "targetID") + targetID, err := strconv.ParseUint(targetIDStr, 10, 32) + if err != nil { + http.Error(w, "Invalid target ID", http.StatusBadRequest) + return + } + + platform := models.PlatformType(chi.URLParam(r, "platform")) + if platform != models.PlatformEntrepreneur && platform != models.PlatformTourist { + http.Error(w, "Invalid platform", http.StatusBadRequest) + return + } + + var req VoteRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + l.Debug("Invalid request body", zap.Error(err)) + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + rating, err := h.service.Vote(r.Context(), userID, uint(targetID), platform, req.Score) + if err != nil { + l.Error("Failed to vote", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(rating) +} + +// GetMyVote возвращает голос текущего пользователя +// @Summary Get current user's vote +// @Tags ratings +// @Produce json +// @Param targetID path int true "Target object ID" +// @Param platform path string true "Platform" +// @Success 200 {object} UserRatingInfoResponse +// @Router /ratings/{targetID}/my-vote/{platform} [get] +func (h *RatingHandler) GetMyVote(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + userID, ok := middleware.GetUserID(r.Context()) + if !ok { + l.Debug("Unauthorized: user ID not found in context") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + targetIDStr := chi.URLParam(r, "targetID") + targetID, err := strconv.ParseUint(targetIDStr, 10, 32) + if err != nil { + http.Error(w, "Invalid target ID", http.StatusBadRequest) + return + } + + platform := models.PlatformType(chi.URLParam(r, "platform")) + if platform != models.PlatformEntrepreneur && platform != models.PlatformTourist { + http.Error(w, "Invalid platform", http.StatusBadRequest) + return + } + + voteInfo, err := h.service.GetUserVote(r.Context(), userID, uint(targetID), platform) + if err != nil { + l.Error("Failed to get user vote", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(voteInfo) +} + +// UpdateMyVote обновляет голос текущего пользователя +// @Summary Update current user's vote +// @Tags ratings +// @Accept json +// @Produce json +// @Param targetID path int true "Target object ID" +// @Param platform path string true "Platform" +// @Param request body VoteRequest true "Vote data" +// @Success 200 {object} RatingResponse +// @Router /ratings/{targetID}/my-vote/{platform} [put] +func (h *RatingHandler) UpdateMyVote(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + userID, ok := middleware.GetUserID(r.Context()) + if !ok { + l.Debug("Unauthorized: user ID not found in context") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + targetIDStr := chi.URLParam(r, "targetID") + targetID, err := strconv.ParseUint(targetIDStr, 10, 32) + if err != nil { + http.Error(w, "Invalid target ID", http.StatusBadRequest) + return + } + + platform := models.PlatformType(chi.URLParam(r, "platform")) + if platform != models.PlatformEntrepreneur && platform != models.PlatformTourist { + http.Error(w, "Invalid platform", http.StatusBadRequest) + return + } + + var req VoteRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + l.Debug("Invalid request body", zap.Error(err)) + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + rating, err := h.service.UpdateUserVote(r.Context(), userID, uint(targetID), platform, req.Score) + if err != nil { + l.Error("Failed to update vote", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(rating) +} + +// DeleteMyVote удаляет голос текущего пользователя +// @Summary Delete current user's vote +// @Tags ratings +// @Param targetID path int true "Target object ID" +// @Param platform path string true "Platform" +// @Success 204 "No Content" +// @Router /ratings/{targetID}/my-vote/{platform} [delete] +func (h *RatingHandler) DeleteMyVote(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + userID, ok := middleware.GetUserID(r.Context()) + if !ok { + l.Debug("Unauthorized: user ID not found in context") + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + targetIDStr := chi.URLParam(r, "targetID") + targetID, err := strconv.ParseUint(targetIDStr, 10, 32) + if err != nil { + http.Error(w, "Invalid target ID", http.StatusBadRequest) + return + } + + platform := models.PlatformType(chi.URLParam(r, "platform")) + if platform != models.PlatformEntrepreneur && platform != models.PlatformTourist { + http.Error(w, "Invalid platform", http.StatusBadRequest) + return + } + + if err := h.service.DeleteUserVote(r.Context(), userID, uint(targetID), platform); err != nil { + l.Error("Failed to delete vote", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// GetRatingStats возвращает статистику по рейтингам +// @Summary Get rating statistics +// @Tags ratings +// @Produce json +// @Success 200 {object} map[string]interface{} +// @Router /ratings/stats [get] +func (h *RatingHandler) GetRatingStats(w http.ResponseWriter, r *http.Request) { + l := logger.Get() + + stats, err := h.service.GetRatingStats(r.Context()) + if err != nil { + l.Error("Failed to get rating stats", zap.Error(err)) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(stats) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/rating/router.go b/main_dc/yalarba/api_yal/internal/domain/rating/router.go new file mode 100644 index 0000000..7a428e5 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/rating/router.go @@ -0,0 +1,45 @@ +package rating + +import ( + "api_yal/internal/logger" + "api_yal/internal/middleware" + "api_yal/internal/repository" + + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +// RegisterRoutes регистрирует маршруты для рейтингов +func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { + l := logger.Get() + l.Info("Registering routes for rating") + + ratingRepo := repository.NewRatingRepository(db) + ratingService := NewRatingServiceImpl(ratingRepo, db) + ratingHandler := NewRatingHandler(ratingService) + + // Группируем маршруты для рейтингов + r.Route("/ratings", func(r chi.Router) { + // Публичные маршруты (не требуют аутентификации) + r.Get("/", ratingHandler.ListRatings) + r.Get("/stats", ratingHandler.GetRatingStats) + r.Get("/{id}", ratingHandler.GetRatingByID) + r.Get("/object/{objectID}/platform/{platform}", ratingHandler.GetRatingByObjectAndPlatform) + + // Защищенные маршруты (требуют аутентификации) + r.Group(func(r chi.Router) { + r.Use(middleware.AuthMiddleware(jwtSecret)) + + // CRUD операции с рейтингами + r.Post("/", ratingHandler.CreateRating) + r.Put("/{id}", ratingHandler.UpdateRating) + r.Delete("/{id}", ratingHandler.DeleteRating) + + // Голосование + r.Post("/{targetID}/vote/{platform}", ratingHandler.Vote) + r.Get("/{targetID}/my-vote/{platform}", ratingHandler.GetMyVote) + r.Put("/{targetID}/my-vote/{platform}", ratingHandler.UpdateMyVote) + r.Delete("/{targetID}/my-vote/{platform}", ratingHandler.DeleteMyVote) + }) + }) +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/rating/service.go b/main_dc/yalarba/api_yal/internal/domain/rating/service.go new file mode 100644 index 0000000..bad8a8d --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/rating/service.go @@ -0,0 +1,600 @@ +package rating + +import ( + "context" + "errors" + "fmt" + "time" + + "api_yal/internal/models" + "api_yal/internal/repository" + "api_yal/internal/logger" + + "go.uber.org/zap" + "gorm.io/gorm" +) + +// RatingService определяет интерфейс сервиса рейтингов +type RatingService interface { + // CreateRating создает новый рейтинг для объекта + CreateRating(ctx context.Context, ownerID uint, req *CreateRatingRequest) (*RatingResponse, error) + + // GetRatingByID возвращает рейтинг по ID + GetRatingByID(ctx context.Context, id uint) (*RatingResponse, error) + + // GetRatingByObjectAndPlatform возвращает рейтинг объекта по платформе + GetRatingByObjectAndPlatform(ctx context.Context, objectID uint, platform models.PlatformType) (*RatingResponse, error) + + // UpdateRating обновляет рейтинг + UpdateRating(ctx context.Context, id uint, req *UpdateRatingRequest) (*RatingResponse, error) + + // DeleteRating удаляет рейтинг + DeleteRating(ctx context.Context, id uint) error + + // ListRatings возвращает список рейтингов с пагинацией + ListRatings(ctx context.Context, req *ListRatingsRequest) (*ListRatingsResponse, error) + + // GetRatingsByObject возвращает все рейтинги объекта + GetRatingsByObject(ctx context.Context, objectID uint) ([]RatingResponse, error) + + // GetRatingsByOwner возвращает рейтинги владельца + GetRatingsByOwner(ctx context.Context, ownerID uint, req *ListRatingsRequest) (*ListRatingsResponse, error) + + // Vote голосование за объект + Vote(ctx context.Context, voterID uint, targetID uint, platform models.PlatformType, score int) (*RatingResponse, error) + + // GetUserVote возвращает голос пользователя + GetUserVote(ctx context.Context, voterID uint, targetID uint, platform models.PlatformType) (*UserRatingInfoResponse, error) + + // UpdateUserVote обновляет голос пользователя + UpdateUserVote(ctx context.Context, voterID uint, targetID uint, platform models.PlatformType, newScore int) (*RatingResponse, error) + + // DeleteUserVote удаляет голос пользователя + DeleteUserVote(ctx context.Context, voterID uint, targetID uint, platform models.PlatformType) error + + // GetRatingStats возвращает статистику по рейтингам + GetRatingStats(ctx context.Context) (map[string]interface{}, error) +} + +type ratingServiceImpl struct { + ratingRepo repository.RatingRepository + db *gorm.DB +} + +// NewRatingServiceImpl создает новый экземпляр сервиса рейтингов +func NewRatingServiceImpl(ratingRepo repository.RatingRepository, db *gorm.DB) RatingService { + return &ratingServiceImpl{ + ratingRepo: ratingRepo, + db: db, + } +} + +// CreateRating создает новый рейтинг для объекта +func (s *ratingServiceImpl) CreateRating(ctx context.Context, ownerID uint, req *CreateRatingRequest) (*RatingResponse, error) { + l := logger.Get() + + // Проверяем, существует ли уже рейтинг для этого объекта и платформы + existing, _ := s.ratingRepo.GetByObjectAndPlatform(req.ObjectID, req.Platform) + if existing != nil { + return nil, errors.New("rating already exists for this object and platform") + } + + rating := &models.Rating{ + OwnerID: ownerID, + ObjectID: req.ObjectID, + Platform: req.Platform, + AverageScore: 0, + TotalVotes: 0, + VoteBreakdown: models.VoteBreakdown{ + Score1: 0, + Score2: 0, + Score3: 0, + Score4: 0, + Score5: 0, + }, + } + + err := s.db.Transaction(func(tx *gorm.DB) error { + // Создаем рейтинг + if err := s.ratingRepo.Create(rating); err != nil { + return err + } + + // Устанавливаем связь VoteBreakdown с рейтингом + rating.VoteBreakdown.RatingID = rating.ID + if err := s.ratingRepo.UpdateVoteBreakdown(&rating.VoteBreakdown); err != nil { + return err + } + + return nil + }) + + if err != nil { + l.Error("Failed to create rating", zap.Error(err)) + return nil, err + } + + return s.toRatingResponse(rating), nil +} + +// GetRatingByID возвращает рейтинг по ID +func (s *ratingServiceImpl) GetRatingByID(ctx context.Context, id uint) (*RatingResponse, error) { + rating, err := s.ratingRepo.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("rating not found") + } + return nil, err + } + return s.toRatingResponse(rating), nil +} + +// GetRatingByObjectAndPlatform возвращает рейтинг объекта по платформе +func (s *ratingServiceImpl) GetRatingByObjectAndPlatform(ctx context.Context, objectID uint, platform models.PlatformType) (*RatingResponse, error) { + rating, err := s.ratingRepo.GetByObjectAndPlatform(objectID, platform) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("rating not found") + } + return nil, err + } + return s.toRatingResponse(rating), nil +} + +// UpdateRating обновляет рейтинг +func (s *ratingServiceImpl) UpdateRating(ctx context.Context, id uint, req *UpdateRatingRequest) (*RatingResponse, error) { + rating, err := s.ratingRepo.GetByID(id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("rating not found") + } + return nil, err + } + + if req.AverageScore != nil { + rating.AverageScore = *req.AverageScore + } + if req.TotalVotes != nil { + rating.TotalVotes = *req.TotalVotes + } + + if err := s.ratingRepo.Update(rating); err != nil { + return nil, err + } + + return s.toRatingResponse(rating), nil +} + +// DeleteRating удаляет рейтинг +func (s *ratingServiceImpl) DeleteRating(ctx context.Context, id uint) error { + // Проверяем существование + if _, err := s.ratingRepo.GetByID(id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("rating not found") + } + return err + } + return s.ratingRepo.Delete(id) +} + +// ListRatings возвращает список рейтингов с пагинацией +func (s *ratingServiceImpl) ListRatings(ctx context.Context, req *ListRatingsRequest) (*ListRatingsResponse, error) { + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 || req.PageSize > 100 { + req.PageSize = 20 + } + + offset := (req.Page - 1) * req.PageSize + + var ratings []models.Rating + var err error + var total int64 + + // Фильтрация по платформе + if req.Platform != "" { + platform := models.PlatformType(req.Platform) + ratings, err = s.ratingRepo.ListByPlatform(platform, offset, req.PageSize) + if err == nil { + total, _ = s.ratingRepo.Count() + } + } else if req.OwnerID > 0 { + ratings, err = s.ratingRepo.ListByOwner(req.OwnerID, offset, req.PageSize) + if err == nil { + total, _ = s.ratingRepo.Count() + } + } else { + ratings, err = s.ratingRepo.List(offset, req.PageSize) + if err == nil { + total, _ = s.ratingRepo.Count() + } + } + + if err != nil { + return nil, err + } + + responses := make([]RatingResponse, len(ratings)) + for i, rating := range ratings { + responses[i] = *s.toRatingResponse(&rating) + } + + totalPages := int(total) / req.PageSize + if int(total)%req.PageSize > 0 { + totalPages++ + } + + return &ListRatingsResponse{ + Ratings: responses, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + TotalPages: totalPages, + }, nil +} + +// GetRatingsByObject возвращает все рейтинги объекта +func (s *ratingServiceImpl) GetRatingsByObject(ctx context.Context, objectID uint) ([]RatingResponse, error) { + ratings, err := s.ratingRepo.ListByObject(objectID) + if err != nil { + return nil, err + } + + responses := make([]RatingResponse, len(ratings)) + for i, rating := range ratings { + responses[i] = *s.toRatingResponse(&rating) + } + + return responses, nil +} + +// GetRatingsByOwner возвращает рейтинги владельца +func (s *ratingServiceImpl) GetRatingsByOwner(ctx context.Context, ownerID uint, req *ListRatingsRequest) (*ListRatingsResponse, error) { + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 || req.PageSize > 100 { + req.PageSize = 20 + } + + offset := (req.Page - 1) * req.PageSize + + ratings, err := s.ratingRepo.ListByOwner(ownerID, offset, req.PageSize) + if err != nil { + return nil, err + } + + total, err := s.ratingRepo.Count() + if err != nil { + total = int64(len(ratings)) + } + + responses := make([]RatingResponse, len(ratings)) + for i, rating := range ratings { + responses[i] = *s.toRatingResponse(&rating) + } + + totalPages := int(total) / req.PageSize + if int(total)%req.PageSize > 0 { + totalPages++ + } + + return &ListRatingsResponse{ + Ratings: responses, + Total: total, + Page: req.Page, + PageSize: req.PageSize, + TotalPages: totalPages, + }, nil +} + +// Vote голосование за объект +func (s *ratingServiceImpl) Vote(ctx context.Context, voterID uint, targetID uint, platform models.PlatformType, score int) (*RatingResponse, error) { + l := logger.Get() + + if score < 1 || score > 5 { + return nil, errors.New("score must be between 1 and 5") + } + + var ratingResponse *RatingResponse + + err := s.db.Transaction(func(tx *gorm.DB) error { + // Получаем или создаем рейтинг + rating, err := s.ratingRepo.GetByObjectAndPlatform(targetID, platform) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // Создаем новый рейтинг + rating = &models.Rating{ + OwnerID: 0, // Будет установлен из объекта + ObjectID: targetID, + Platform: platform, + AverageScore: 0, + TotalVotes: 0, + VoteBreakdown: models.VoteBreakdown{ + Score1: 0, + Score2: 0, + Score3: 0, + Score4: 0, + Score5: 0, + }, + } + if err := s.ratingRepo.Create(rating); err != nil { + return err + } + rating.VoteBreakdown.RatingID = rating.ID + if err := s.ratingRepo.UpdateVoteBreakdown(&rating.VoteBreakdown); err != nil { + return err + } + } else { + return err + } + } + + // Проверяем, не голосовал ли уже пользователь + existingVote, _ := s.ratingRepo.GetRatingVote(targetID, voterID, platform) + if existingVote != nil { + return errors.New("user has already voted for this target") + } + + // Создаем голос + vote := &models.RatingVote{ + Platform: platform, + TargetID: targetID, + VoterID: voterID, + Score: score, + } + + if err := s.ratingRepo.CreateRatingVote(vote); err != nil { + return err + } + + // Обновляем детализацию голосов + breakdown := &rating.VoteBreakdown + switch score { + case 1: + breakdown.Score1++ + case 2: + breakdown.Score2++ + case 3: + breakdown.Score3++ + case 4: + breakdown.Score4++ + case 5: + breakdown.Score5++ + } + + rating.TotalVotes++ + rating.AverageScore = s.ratingRepo.CalculateAverageScore(breakdown) + + if err := s.ratingRepo.UpdateVoteBreakdown(breakdown); err != nil { + return err + } + + if err := s.ratingRepo.Update(rating); err != nil { + return err + } + + ratingResponse = s.toRatingResponse(rating) + return nil + }) + + if err != nil { + l.Error("Failed to process vote", zap.Error(err)) + return nil, err + } + + return ratingResponse, nil +} + +// GetUserVote возвращает голос пользователя +func (s *ratingServiceImpl) GetUserVote(ctx context.Context, voterID uint, targetID uint, platform models.PlatformType) (*UserRatingInfoResponse, error) { + vote, err := s.ratingRepo.GetRatingVote(targetID, voterID, platform) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return &UserRatingInfoResponse{HasVoted: false}, nil + } + return nil, err + } + + return &UserRatingInfoResponse{ + HasVoted: true, + UserScore: &vote.Score, + }, nil +} + +// UpdateUserVote обновляет голос пользователя +func (s *ratingServiceImpl) UpdateUserVote(ctx context.Context, voterID uint, targetID uint, platform models.PlatformType, newScore int) (*RatingResponse, error) { + l := logger.Get() + + if newScore < 1 || newScore > 5 { + return nil, errors.New("score must be between 1 and 5") + } + + var ratingResponse *RatingResponse + + err := s.db.Transaction(func(tx *gorm.DB) error { + // Получаем существующий голос + vote, err := s.ratingRepo.GetRatingVote(targetID, voterID, platform) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("vote not found") + } + return err + } + + // Получаем рейтинг + rating, err := s.ratingRepo.GetByObjectAndPlatform(targetID, platform) + if err != nil { + return err + } + + oldScore := vote.Score + + // Обновляем детализацию голосов + breakdown := &rating.VoteBreakdown + + // Удаляем старую оценку + switch oldScore { + case 1: + breakdown.Score1-- + case 2: + breakdown.Score2-- + case 3: + breakdown.Score3-- + case 4: + breakdown.Score4-- + case 5: + breakdown.Score5-- + } + + // Добавляем новую оценку + switch newScore { + case 1: + breakdown.Score1++ + case 2: + breakdown.Score2++ + case 3: + breakdown.Score3++ + case 4: + breakdown.Score4++ + case 5: + breakdown.Score5++ + } + + // Обновляем голос + vote.Score = newScore + if err := s.ratingRepo.UpdateRatingVote(vote); err != nil { + return err + } + + // Обновляем рейтинг + rating.AverageScore = s.ratingRepo.CalculateAverageScore(breakdown) + if err := s.ratingRepo.UpdateVoteBreakdown(breakdown); err != nil { + return err + } + if err := s.ratingRepo.Update(rating); err != nil { + return err + } + + ratingResponse = s.toRatingResponse(rating) + return nil + }) + + if err != nil { + l.Error("Failed to update vote", zap.Error(err)) + return nil, err + } + + return ratingResponse, nil +} + +// DeleteUserVote удаляет голос пользователя +func (s *ratingServiceImpl) DeleteUserVote(ctx context.Context, voterID uint, targetID uint, platform models.PlatformType) error { + l := logger.Get() + + err := s.db.Transaction(func(tx *gorm.DB) error { + // Получаем существующий голос + vote, err := s.ratingRepo.GetRatingVote(targetID, voterID, platform) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("vote not found") + } + return err + } + + // Получаем рейтинг + rating, err := s.ratingRepo.GetByObjectAndPlatform(targetID, platform) + if err != nil { + return err + } + + // Обновляем детализацию голосов + breakdown := &rating.VoteBreakdown + switch vote.Score { + case 1: + breakdown.Score1-- + case 2: + breakdown.Score2-- + case 3: + breakdown.Score3-- + case 4: + breakdown.Score4-- + case 5: + breakdown.Score5-- + } + + rating.TotalVotes-- + rating.AverageScore = s.ratingRepo.CalculateAverageScore(breakdown) + + // Удаляем голос + if err := s.ratingRepo.DeleteRatingVote(vote.ID); err != nil { + return err + } + + // Обновляем рейтинг + if err := s.ratingRepo.UpdateVoteBreakdown(breakdown); err != nil { + return err + } + if err := s.ratingRepo.Update(rating); err != nil { + return err + } + + return nil + }) + + if err != nil { + l.Error("Failed to delete vote", zap.Error(err)) + return err + } + + return nil +} + +// GetRatingStats возвращает статистику по рейтингам +func (s *ratingServiceImpl) GetRatingStats(ctx context.Context) (map[string]interface{}, error) { + totalCount, err := s.ratingRepo.Count() + if err != nil { + return nil, err + } + + stats := map[string]interface{}{ + "total_ratings": totalCount, + "timestamp": time.Now(), + } + + return stats, nil +} + +// toRatingResponse конвертирует модель Rating в RatingResponse +func (s *ratingServiceImpl) toRatingResponse(rating *models.Rating) *RatingResponse { + resp := &RatingResponse{ + ID: rating.ID, + OwnerID: rating.OwnerID, + ObjectID: rating.ObjectID, + Platform: rating.Platform, + AverageScore: rating.AverageScore, + TotalVotes: rating.TotalVotes, + CreatedAt: rating.CreatedAt, + UpdatedAt: rating.UpdatedAt, + } + + if rating.VoteBreakdown.ID != 0 { + resp.VoteBreakdown = &VoteBreakdownResponse{ + Score1: rating.VoteBreakdown.Score1, + Score2: rating.VoteBreakdown.Score2, + Score3: rating.VoteBreakdown.Score3, + Score4: rating.VoteBreakdown.Score4, + Score5: rating.VoteBreakdown.Score5, + } + } + + if rating.Object.ID != 0 { + resp.Object = &ObjectBriefResponse{ + ID: rating.Object.ID, + Name: fmt.Sprintf("Object %d", rating.Object.ID), // В реальности нужно брать из Object.Name + } + } + + return resp +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/router/router.go b/main_dc/yalarba/api_yal/internal/router/router.go index c5c4939..9883e5e 100644 --- a/main_dc/yalarba/api_yal/internal/router/router.go +++ b/main_dc/yalarba/api_yal/internal/router/router.go @@ -7,6 +7,7 @@ import ( "api_yal/internal/domain/comment" "api_yal/internal/domain/feetback" "api_yal/internal/domain/object" + "api_yal/internal/domain/rating" "api_yal/internal/logger" "time" @@ -57,14 +58,19 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { // Регистрируем маршруты аккаунтов account.RegisterRoutes(r, db, config.JWTSecret) - // Регистрируем маршруты объектов + // Регистрируем маршурты обьектов object.RegisterRoutes(r, db, config.JWTSecret) - + // Регистрируем маршруты отзывов feetback.RegisterRoutes(r, db, config.JWTSecret) - - // Регистрируем маршруты комментариев + + // Регистрация маршрутов для комментариев comment.RegisterRoutes(r, db, config.JWTSecret) + + // Регистрация маршрутов для райтинга + rating.RegisterRoutes(r, db, config.JWTSecret) + + }) zapLogger.Info("Настройка маршрутов завершена")