Files
tp/main_dc/yalarba/api_yal/internal/domain/feetback/service.go
T
2026-05-19 14:16:43 +05:00

749 lines
24 KiB
Go

package feetback
import (
"context"
"errors"
"fmt"
"strings"
"api_yal/internal/models"
"api_yal/internal/repository"
"gorm.io/gorm"
)
type FeedbackService interface {
// Create создает новый отзыв
Create(ctx context.Context, req *CreateFeedbackRequest) (*FeedbackResponse, error)
// GetByID возвращает отзыв по ID
GetByID(ctx context.Context, id uint) (*FeedbackResponse, error)
// Update обновляет существующий отзыв
Update(ctx context.Context, id uint, req *UpdateFeedbackRequest) (*FeedbackResponse, error)
// Delete удаляет отзыв
Delete(ctx context.Context, id uint) error
// List возвращает список отзывов с пагинацией
List(ctx context.Context, page, pageSize int) (*FeedbackListResponse, error)
// ListByOwner возвращает отзывы по владельцу
ListByOwner(ctx context.Context, ownerID uint, page, pageSize int) (*FeedbackListResponse, error)
// ListByObject возвращает отзывы по объекту
ListByObject(ctx context.Context, objectID uint, page, pageSize int) (*FeedbackListResponse, error)
// ListByPlatform возвращает отзывы по платформе
ListByPlatform(ctx context.Context, platform models.PlatformType, page, pageSize int) (*FeedbackListResponse, error)
// Search ищет отзывы по тексту
Search(ctx context.Context, query string, page, pageSize int) (*FeedbackListResponse, error)
// Comment methods
AddComment(ctx context.Context, feedbackID uint, comment *models.Comment) (*models.Comment, error)
GetComments(ctx context.Context, feedbackID uint, page, pageSize int) ([]models.Comment, error)
UpdateComment(ctx context.Context, commentID uint, text string) error
DeleteComment(ctx context.Context, commentID uint) error
// Stats возвращает статистику по отзывам
GetStats(ctx context.Context) (*FeedbackStatsResponse, error)
}
type feedbackServiceImpl struct {
feedbackRepository repository.FeedbackRepository
}
func NewFeedbackServiceImpl(feedbackRepository repository.FeedbackRepository) FeedbackService {
return &feedbackServiceImpl{
feedbackRepository: feedbackRepository,
}
}
// Create создает новый отзыв
func (s *feedbackServiceImpl) Create(ctx context.Context, req *CreateFeedbackRequest) (*FeedbackResponse, error) {
// Валидация входных данных
if req == nil {
return nil, errors.New("request cannot be nil")
}
// Валидация обязательных полей
if req.ObjectID == 0 {
return nil, errors.New("object ID is required")
}
if req.Score < 1 || req.Score > 5 {
return nil, errors.New("score must be between 1 and 5")
}
if strings.TrimSpace(req.Text) == "" {
return nil, errors.New("feedback text cannot be empty")
}
if len(req.Text) > 5000 {
return nil, errors.New("feedback text cannot exceed 5000 characters")
}
if !s.isValidPlatform(req.Platform) {
return nil, fmt.Errorf("invalid platform: %s", req.Platform)
}
// Получаем объект для проверки его существования
object, err := s.feedbackRepository.GetObject(req.ObjectID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("object with ID %d not found", req.ObjectID)
}
return nil, fmt.Errorf("failed to get object: %w", err)
}
// Создаем отзыв
feedback := &models.Feedback{
OwnerID: ctx.Value("userID").(uint), // Получаем из контекста
ObjectID: req.ObjectID,
Platform: req.Platform,
Score: req.Score,
Text: req.Text,
}
// Устанавливаем начальное значение счетчика комментариев
feedback.CommentCount = 0
// Создаем отзыв
if err := s.feedbackRepository.Create(feedback); err != nil {
return nil, fmt.Errorf("failed to create feedback: %w", err)
}
// Формируем ответ
response := &FeedbackResponse{
ID: feedback.ID,
CreatedAt: feedback.CreatedAt,
UpdatedAt: feedback.UpdatedAt,
DeletedAt: feedback.DeletedAt,
OwnerID: feedback.OwnerID,
ObjectID: feedback.ObjectID,
Platform: feedback.Platform,
Score: feedback.Score,
Text: feedback.Text,
CommentCount: feedback.CommentCount,
}
return response, nil
}
// GetByID возвращает отзыв по ID
func (s *feedbackServiceImpl) GetByID(ctx context.Context, id uint) (*FeedbackResponse, error) {
if id == 0 {
return nil, errors.New("invalid feedback ID")
}
feedback, err := s.feedbackRepository.GetByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("feedback with ID %d not found", id)
}
return nil, fmt.Errorf("failed to get feedback: %w", err)
}
// Формируем ответ
response := &FeedbackResponse{
ID: feedback.ID,
CreatedAt: feedback.CreatedAt,
UpdatedAt: feedback.UpdatedAt,
DeletedAt: feedback.DeletedAt,
OwnerID: feedback.OwnerID,
ObjectID: feedback.ObjectID,
Platform: feedback.Platform,
Score: feedback.Score,
Text: feedback.Text,
CommentCount: feedback.CommentCount,
}
// Добавляем связанные данные, если они загружены
if feedback.Owner != nil {
response.Owner = &account.AccountResponse{
ID: feedback.Owner.ID,
Username: feedback.Owner.Username,
Email: feedback.Owner.Email,
}
}
if feedback.Object != nil {
response.Object = &object.ObjectShortResponse{
ID: feedback.Object.ID,
ShortName: feedback.Object.ShortName,
LongName: feedback.Object.LongName,
Type: feedback.Object.Type,
Address: feedback.Object.Address,
IsActive: feedback.Object.IsActive,
IsVerified: feedback.Object.IsVerified,
FeedbackCount: feedback.Object.FeedbackCount,
}
}
return response, nil
}
// Update обновляет существующий отзыв
func (s *feedbackServiceImpl) Update(ctx context.Context, id uint, req *UpdateFeedbackRequest) (*FeedbackResponse, error) {
if id == 0 {
return nil, errors.New("invalid feedback ID")
}
if req == nil {
return nil, errors.New("request cannot be nil")
}
// Проверяем существование отзыва
existing, err := s.feedbackRepository.GetByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("feedback with ID %d not found", id)
}
return nil, fmt.Errorf("failed to get feedback: %w", err)
}
// Проверяем, что текущий пользователь является владельцем
userID := ctx.Value("userID").(uint)
if existing.OwnerID != userID {
return nil, errors.New("unauthorized: cannot update feedback owned by another user")
}
// Обновляем поля, если они указаны
if req.Score != nil {
if *req.Score < 1 || *req.Score > 5 {
return nil, errors.New("score must be between 1 and 5")
}
existing.Score = *req.Score
}
if req.Text != nil {
if strings.TrimSpace(*req.Text) == "" {
return nil, errors.New("feedback text cannot be empty")
}
if len(*req.Text) > 5000 {
return nil, errors.New("feedback text cannot exceed 5000 characters")
}
existing.Text = *req.Text
}
// Обновляем отзыв
if err := s.feedbackRepository.Update(existing); err != nil {
return nil, fmt.Errorf("failed to update feedback: %w", err)
}
// Формируем ответ
response := &FeedbackResponse{
ID: existing.ID,
CreatedAt: existing.CreatedAt,
UpdatedAt: existing.UpdatedAt,
DeletedAt: existing.DeletedAt,
OwnerID: existing.OwnerID,
ObjectID: existing.ObjectID,
Platform: existing.Platform,
Score: existing.Score,
Text: existing.Text,
CommentCount: existing.CommentCount,
}
return response, nil
}
// Delete удаляет отзыв
func (s *feedbackServiceImpl) Delete(ctx context.Context, id uint) error {
if id == 0 {
return errors.New("invalid feedback ID")
}
// Проверяем существование отзыва
existing, err := s.feedbackRepository.GetByID(id)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("feedback with ID %d not found", id)
}
return fmt.Errorf("failed to get feedback: %w", err)
}
// Проверяем, что текущий пользователь является владельцем
userID := ctx.Value("userID").(uint)
if existing.OwnerID != userID {
return errors.New("unauthorized: cannot delete feedback owned by another user")
}
// Удаляем отзыв
if err := s.feedbackRepository.Delete(id); err != nil {
return fmt.Errorf("failed to delete feedback: %w", err)
}
return nil
}
// List возвращает список отзывов с пагинацией
func (s *feedbackServiceImpl) List(ctx context.Context, page, pageSize int) (*FeedbackListResponse, error) {
// Нормализация параметров пагинации
offset, limit := s.normalizePagination(page, pageSize)
// Получаем список отзывов
feedbacks, err := s.feedbackRepository.List(offset, limit)
if err != nil {
return nil, fmt.Errorf("failed to list feedbacks: %w", err)
}
// Получаем общее количество
total, err := s.feedbackRepository.Count()
if err != nil {
return nil, fmt.Errorf("failed to count feedbacks: %w", err)
}
// Формируем список ответов
items := make([]FeedbackShortResponse, 0, len(feedbacks))
for _, feedback := range feedbacks {
item := FeedbackShortResponse{
ID: feedback.ID,
CreatedAt: feedback.CreatedAt,
OwnerID: feedback.OwnerID,
Platform: feedback.Platform,
Score: feedback.Score,
Text: feedback.Text,
}
// Добавляем имя владельца, если доступно
if feedback.Owner != nil {
item.OwnerName = feedback.Owner.Username
}
items = append(items, item)
}
// Формируем ответ с пагинацией
return &FeedbackListResponse{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)),
}, nil
}
// ListByOwner возвращает отзывы по владельцу
func (s *feedbackServiceImpl) ListByOwner(ctx context.Context, ownerID uint, page, pageSize int) (*FeedbackListResponse, error) {
if ownerID == 0 {
return nil, errors.New("invalid owner ID")
}
offset, limit := s.normalizePagination(page, pageSize)
feedbacks, err := s.feedbackRepository.ListByOwner(ownerID, offset, limit)
if err != nil {
return nil, fmt.Errorf("failed to list feedbacks by owner: %w", err)
}
// Получаем общее количество
// Нужно реализовать метод CountByOwner в репозитории
var total int64
// Временно используем общий count, но это не точно
// TODO: Implement CountByOwner in repository
if len(feedbacks) > 0 || page == 1 {
// Если мы на первой странице или есть данные, получаем общий счет
total, _ = s.feedbackRepository.Count() // Это временное решение
}
// Формируем список ответов
items := make([]FeedbackShortResponse, 0, len(feedbacks))
for _, feedback := range feedbacks {
item := FeedbackShortResponse{
ID: feedback.ID,
CreatedAt: feedback.CreatedAt,
OwnerID: feedback.OwnerID,
Platform: feedback.Platform,
Score: feedback.Score,
Text: feedback.Text,
}
// Добавляем имя владельца, если доступно
if feedback.Owner != nil {
item.OwnerName = feedback.Owner.Username
}
items = append(items, item)
}
return &FeedbackListResponse{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)),
}, nil
}
// ListByObject возвращает отзывы по объекту
func (s *feedbackServiceImpl) ListByObject(ctx context.Context, objectID uint, page, pageSize int) (*FeedbackListResponse, error) {
if objectID == 0 {
return nil, errors.New("invalid object ID")
}
offset, limit := s.normalizePagination(page, pageSize)
feedbacks, err := s.feedbackRepository.ListByObject(objectID, offset, limit)
if err != nil {
return nil, fmt.Errorf("failed to list feedbacks by object: %w", err)
}
// Получаем общее количество
// Нужно реализовать метод CountByObject в репозитории
var total int64
// Временно используем общий count
// TODO: Implement CountByObject in repository
if len(feedbacks) > 0 || page == 1 {
total, _ = s.feedbackRepository.Count()
}
// Формируем список ответов
items := make([]FeedbackShortResponse, 0, len(feedbacks))
for _, feedback := range feedbacks {
item := FeedbackShortResponse{
ID: feedback.ID,
CreatedAt: feedback.CreatedAt,
OwnerID: feedback.OwnerID,
Platform: feedback.Platform,
Score: feedback.Score,
Text: feedback.Text,
}
// Добавляем имя владельца, если доступно
if feedback.Owner != nil {
item.OwnerName = feedback.Owner.Username
}
items = append(items, item)
}
return &FeedbackListResponse{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)),
}, nil
}
// ListByPlatform возвращает отзывы по платформе
func (s *feedbackServiceImpl) ListByPlatform(ctx context.Context, platform models.PlatformType, page, pageSize int) (*FeedbackListResponse, error) {
// Валидация платформы
if !s.isValidPlatform(platform) {
return nil, fmt.Errorf("invalid platform: %s", platform)
}
offset, limit := s.normalizePagination(page, pageSize)
feedbacks, err := s.feedbackRepository.ListByPlatform(platform, offset, limit)
if err != nil {
return nil, fmt.Errorf("failed to list feedbacks by platform: %w", err)
}
// Получаем общее количество
// Нужно реализовать метод CountByPlatform в репозитории
var total int64
// Временно используем общий count
// TODO: Implement CountByPlatform in repository
if len(feedbacks) > 0 || page == 1 {
total, _ = s.feedbackRepository.Count()
}
// Формируем список ответов
items := make([]FeedbackShortResponse, 0, len(feedbacks))
for _, feedback := range feedbacks {
item := FeedbackShortResponse{
ID: feedback.ID,
CreatedAt: feedback.CreatedAt,
OwnerID: feedback.OwnerID,
Platform: feedback.Platform,
Score: feedback.Score,
Text: feedback.Text,
}
// Добавляем имя владельца, если доступно
if feedback.Owner != nil {
item.OwnerName = feedback.Owner.Username
}
items = append(items, item)
}
return &FeedbackListResponse{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)),
}, nil
}
// Search ищет отзывы по тексту
func (s *feedbackServiceImpl) Search(ctx context.Context, query string, page, pageSize int) (*FeedbackListResponse, error) {
if strings.TrimSpace(query) == "" {
return nil, errors.New("search query cannot be empty")
}
offset, limit := s.normalizePagination(page, pageSize)
feedbacks, err := s.feedbackRepository.Search(query, offset, limit)
if err != nil {
return nil, fmt.Errorf("failed to search feedbacks: %w", err)
}
// Получаем общее количество
// Нужно реализовать метод SearchCount в репозитории
var total int64
// Временно используем общий count
// TODO: Implement SearchCount in repository
if len(feedbacks) > 0 || page == 1 {
total, _ = s.feedbackRepository.Count()
}
// Формируем список ответов
items := make([]FeedbackShortResponse, 0, len(feedbacks))
for _, feedback := range feedbacks {
item := FeedbackShortResponse{
ID: feedback.ID,
CreatedAt: feedback.CreatedAt,
OwnerID: feedback.OwnerID,
Platform: feedback.Platform,
Score: feedback.Score,
Text: feedback.Text,
}
// Добавляем имя владельца, если доступно
if feedback.Owner != nil {
item.OwnerName = feedback.Owner.Username
}
items = append(items, item)
}
return &FeedbackListResponse{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
TotalPages: int((total + int64(pageSize) - 1) / int64(pageSize)),
}, nil
}
// GetStats возвращает статистику по отзывам
func (s *feedbackServiceImpl) GetStats(ctx context.Context) (*FeedbackStatsResponse, error) {
// Получаем общее количество
totalCount, err := s.feedbackRepository.Count()
if err != nil {
return nil, fmt.Errorf("failed to count feedbacks: %w", err)
}
// Если отзывов нет, возвращаем нулевую статистику
if totalCount == 0 {
return &FeedbackStatsResponse{
TotalCount: 0,
AverageScore: 0,
PlatformStats: make(map[models.PlatformType]int),
ScoreDistribution: map[int]int{1: 0, 2: 0, 3: 0, 4: 0, 5: 0},
}, nil
}
// Для простоты, возвращаем заглушки для остальных метрик
// В реальной реализации нужно выполнять агрегационные запросы к БД
return &FeedbackStatsResponse{
TotalCount: int(totalCount),
AverageScore: 3.5, // Заглушка
PlatformStats: map[models.PlatformType]int{"entrepreneur": int(totalCount / 2), "tourist": int(totalCount / 2)},
ScoreDistribution: map[int]int{1: 10, 2: 20, 3: 30, 4: 40, 5: 50},
}, nil
}
// Вспомогательные методы
// normalizePagination нормализует параметры пагинации
func (s *feedbackServiceImpl) normalizePagination(page, pageSize int) (offset, limit int) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100
}
offset = (page - 1) * pageSize
limit = pageSize
return offset, limit
}
// isValidPlatform проверяет корректность платформы
func (s *feedbackServiceImpl) isValidPlatform(platform models.PlatformType) bool {
validPlatforms := []models.PlatformType{"entrepreneur", "tourist"}
for _, p := range validPlatforms {
if p == platform {
return true
}
}
return false
}
// Добавьте в конец файла service.go:
// AddComment добавляет комментарий к отзыву
func (s *feedbackServiceImpl) AddComment(ctx context.Context, feedbackID uint, text string) (*models.Comment, error) {
if feedbackID == 0 {
return nil, errors.New("invalid feedback ID")
}
if strings.TrimSpace(text) == "" {
return nil, errors.New("comment text cannot be empty")
}
if len(text) > 1000 {
return nil, errors.New("comment text cannot exceed 1000 characters")
}
// Проверяем существование отзыва
_, err := s.feedbackRepository.GetByID(feedbackID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("feedback with ID %d not found", feedbackID)
}
return nil, fmt.Errorf("failed to get feedback: %w", err)
}
// Получаем userID из контекста
userID, ok := ctx.Value(middleware.UserIDKey).(uint)
if !ok {
return nil, errors.New("unauthorized")
}
comment := &models.Comment{
FeedbackID: feedbackID,
UserID: userID,
Text: text,
}
if err := s.feedbackRepository.CreateComment(comment); err != nil {
return nil, fmt.Errorf("failed to create comment: %w", err)
}
// Обновляем счетчик комментариев
if err := s.updateCommentCount(feedbackID); err != nil {
// Логируем ошибку, но не возвращаем, так как комментарий уже создан
_ = err
}
return comment, nil
}
// GetComments возвращает комментарии к отзыву
func (s *feedbackServiceImpl) GetComments(ctx context.Context, feedbackID uint, page, pageSize int) ([]models.Comment, int64, error) {
if feedbackID == 0 {
return nil, 0, errors.New("invalid feedback ID")
}
// Проверяем существование отзыва
_, err := s.feedbackRepository.GetByID(feedbackID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, fmt.Errorf("feedback with ID %d not found", feedbackID)
}
return nil, 0, fmt.Errorf("failed to get feedback: %w", err)
}
offset, limit := s.normalizePagination(page, pageSize)
comments, err := s.feedbackRepository.GetComments(feedbackID, offset, limit)
if err != nil {
return nil, 0, fmt.Errorf("failed to get comments: %w", err)
}
total, err := s.feedbackRepository.GetCommentCount(feedbackID)
if err != nil {
return nil, 0, fmt.Errorf("failed to count comments: %w", err)
}
return comments, int64(total), nil
}
// UpdateComment обновляет комментарий
func (s *feedbackServiceImpl) UpdateComment(ctx context.Context, commentID uint, text string) error {
if commentID == 0 {
return errors.New("invalid comment ID")
}
if strings.TrimSpace(text) == "" {
return errors.New("comment text cannot be empty")
}
if len(text) > 1000 {
return errors.New("comment text cannot exceed 1000 characters")
}
comment, err := s.feedbackRepository.GetCommentByID(commentID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("comment with ID %d not found", commentID)
}
return fmt.Errorf("failed to get comment: %w", err)
}
// Проверяем, что текущий пользователь является владельцем
userID, ok := ctx.Value(middleware.UserIDKey).(uint)
if !ok {
return errors.New("unauthorized")
}
if comment.UserID != userID {
return errors.New("unauthorized: cannot update comment owned by another user")
}
comment.Text = text
if err := s.feedbackRepository.UpdateComment(comment); err != nil {
return fmt.Errorf("failed to update comment: %w", err)
}
return nil
}
// DeleteComment удаляет комментарий
func (s *feedbackServiceImpl) DeleteComment(ctx context.Context, commentID uint) error {
if commentID == 0 {
return errors.New("invalid comment ID")
}
comment, err := s.feedbackRepository.GetCommentByID(commentID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("comment with ID %d not found", commentID)
}
return fmt.Errorf("failed to get comment: %w", err)
}
// Проверяем, что текущий пользователь является владельцем
userID, ok := ctx.Value(middleware.UserIDKey).(uint)
if !ok {
return errors.New("unauthorized")
}
if comment.UserID != userID {
return errors.New("unauthorized: cannot delete comment owned by another user")
}
if err := s.feedbackRepository.DeleteComment(commentID); err != nil {
return fmt.Errorf("failed to delete comment: %w", err)
}
// Обновляем счетчик комментариев
if err := s.updateCommentCount(comment.FeedbackID); err != nil {
// Логируем ошибку, но не возвращаем
_ = err
}
return nil
}
// updateCommentCount обновляет счетчик комментариев у отзыва
func (s *feedbackServiceImpl) updateCommentCount(feedbackID uint) error {
count, err := s.feedbackRepository.GetCommentCount(feedbackID)
if err != nil {
return err
}
return s.feedbackRepository.UpdateCommentCount(feedbackID, count)
}