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 }