ba2e3b9545
modified: main_dc/yalarba/api_yal/internal/domain/rating/dto.go new file: main_dc/yalarba/api_yal/internal/domain/rating/handler.go new file: main_dc/yalarba/api_yal/internal/domain/rating/router.go new file: main_dc/yalarba/api_yal/internal/domain/rating/service.go modified: main_dc/yalarba/api_yal/internal/router/router.go add raing domain without test
600 lines
16 KiB
Go
600 lines
16 KiB
Go
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
|
|
} |