package object import ( "api_yal/internal/domain/account" "api_yal/internal/models" "api_yal/internal/repository" "context" "errors" "fmt" "gorm.io/gorm" ) type ObjectService interface { GetObjectByID(ctx context.Context, id uint) (*ObjectResponse, error) CreateObject(ctx context.Context, req *CreateObjectRequest) (*ObjectResponse, error) UpdateObject(ctx context.Context, id uint, req *UpdateObjectRequest) (*ObjectResponse, error) DeleteObject(ctx context.Context, id uint) error ListObjects(ctx context.Context, req *ListObjectsRequest) (*ObjectListResponse, error) GetObjectsByOwner(ctx context.Context, ownerID uint, page, pageSize int) (*ObjectListResponse, error) GetObjectsByType(ctx context.Context, objectType string, page, pageSize int) (*ObjectListResponse, error) SearchObjects(ctx context.Context, query string, page, pageSize int) (*ObjectListResponse, error) GetNearbyObjects(ctx context.Context, lat, lng, radius float64, page, pageSize int) (*ObjectListResponse, error) ToggleVerification(ctx context.Context, id uint, verified bool) error // Feedback methods CreateFeedback(ctx context.Context, req *CreateFeedbackRequest, ownerID uint) (*FeedbackResponse, error) UpdateFeedback(ctx context.Context, id uint, req *UpdateFeedbackRequest, ownerID uint) (*FeedbackResponse, error) DeleteFeedback(ctx context.Context, id uint, ownerID uint) error GetFeedbackByID(ctx context.Context, id uint) (*FeedbackResponse, error) GetFeedbacksByObject(ctx context.Context, objectID uint, page, pageSize int) (*FeedbackListResponse, error) // Rating methods CreateRatingVote(ctx context.Context, req *CreateRatingVoteRequest, voterID uint) (*RatingVoteResponse, error) GetObjectRating(ctx context.Context, objectID uint, platform models.PlatformType) (*RatingResponse, error) GetUserRatingVote(ctx context.Context, objectID uint, userID uint, platform models.PlatformType) (*RatingVoteResponse, error) } type objectServiceImpl struct { objectRepository repository.ObjectRepository } func NewObjectService(objectRepository repository.ObjectRepository) ObjectService { return &objectServiceImpl{ objectRepository: objectRepository, } } // GetObjectByID возвращает объект по ID с полной информацией func (s *objectServiceImpl) GetObjectByID(ctx context.Context, id uint) (*ObjectResponse, error) { object, err := s.objectRepository.GetByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrObjectNotFound } return nil, fmt.Errorf("failed to get object by id: %w", err) } // Получаем дополнительные данные owner, _ := s.objectRepository.GetOwner(id) touristRating, _ := s.objectRepository.GetTouristRating(id) entrepreneurRating, _ := s.objectRepository.GetEntrepreneurRating(id) feedbacks, _ := s.objectRepository.GetFeedbacks(id, 0, 5) // Последние 5 отзывов return s.mapToObjectResponse(object, owner, touristRating, entrepreneurRating, feedbacks), nil } // CreateObject создает новый объект func (s *objectServiceImpl) CreateObject(ctx context.Context, req *CreateObjectRequest) (*ObjectResponse, error) { // Валидация if err := s.validateCreateRequest(req); err != nil { return nil, err } // Устанавливаем значения по умолчанию isActive := true if req.IsActive != nil { isActive = *req.IsActive } isVerified := false if req.IsVerified != nil { isVerified = *req.IsVerified } title := req.Title if title == "" { title = req.ShortName } status := models.ObjectStatusActive if req.Status != "" { status = models.ObjectStatus(req.Status) } object := &models.Object{ OwnerID: req.OwnerID, Title: title, ShortName: req.ShortName, LongName: req.LongName, Type: req.Type, Price: req.Price, PricePeriod: req.PricePeriod, Phone: req.Phone, Email: req.Email, Site: req.Site, ShortDescription: req.ShortDescription, Description: req.Description, Address: req.Address, Latitude: req.Latitude, Longitude: req.Longitude, IsActive: isActive, IsVerified: isVerified, Status: status, ViewCount: 0, FeedbackCount: 0, } if err := s.objectRepository.Create(object); err != nil { return nil, fmt.Errorf("failed to create object: %w", err) } // Создаем начальные записи рейтингов s.initializeRatings(object.ID) return s.GetObjectByID(ctx, object.ID) } // UpdateObject обновляет существующий объект func (s *objectServiceImpl) UpdateObject(ctx context.Context, id uint, req *UpdateObjectRequest) (*ObjectResponse, error) { // Проверяем существование объекта existing, err := s.objectRepository.GetByID(id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrObjectNotFound } return nil, fmt.Errorf("failed to get object: %w", err) } // Применяем изменения s.applyUpdates(existing, req) if err := s.objectRepository.Update(existing); err != nil { return nil, fmt.Errorf("failed to update object: %w", err) } return s.GetObjectByID(ctx, id) } // DeleteObject мягко удаляет объект func (s *objectServiceImpl) DeleteObject(ctx context.Context, id uint) error { // Проверяем существование if _, err := s.objectRepository.GetByID(id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrObjectNotFound } return fmt.Errorf("failed to get object: %w", err) } if err := s.objectRepository.Delete(id); err != nil { return fmt.Errorf("failed to delete object: %w", err) } return nil } // ListObjects возвращает список объектов с пагинацией и фильтрацией func (s *objectServiceImpl) ListObjects(ctx context.Context, req *ListObjectsRequest) (*ObjectListResponse, error) { // Устанавливаем значения по умолчанию page := req.Page if page < 1 { page = 1 } pageSize := req.PageSize if pageSize < 1 { pageSize = 10 } if pageSize > 100 { pageSize = 100 } offset := (page - 1) * pageSize var objects []models.Object var total int64 var err error // Применяем фильтры switch { case req.ObjectStatus != "": objects, err = s.objectRepository.ListByObjectStatus(req.ObjectStatus, offset, pageSize) if err == nil { total, _ = s.countObjectsByStatusString(req.ObjectStatus) } case req.Type != "": objects, err = s.objectRepository.ListByType(req.Type, offset, pageSize) if err == nil { total, _ = s.countObjectsByType(req.Type) } case req.Status != nil: objects, err = s.objectRepository.ListByStatus(*req.Status, offset, pageSize) if err == nil { total, _ = s.countObjectsByStatus(*req.Status) } case req.Query != "": objects, err = s.objectRepository.Search(req.Query, offset, pageSize) if err == nil { total, _ = s.countObjectsBySearch(req.Query) } default: objects, err = s.objectRepository.List(offset, pageSize) if err == nil { total, _ = s.objectRepository.Count() } } if err != nil { return nil, fmt.Errorf("failed to list objects: %w", err) } items := make([]ObjectShortResponse, len(objects)) for i, obj := range objects { items[i] = s.mapToObjectShortResponse(&obj) } totalPages := int(total) / pageSize if int(total)%pageSize > 0 { totalPages++ } return &ObjectListResponse{ Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, }, nil } // GetObjectsByOwner возвращает объекты владельца func (s *objectServiceImpl) GetObjectsByOwner(ctx context.Context, ownerID uint, page, pageSize int) (*ObjectListResponse, error) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 10 } if pageSize > 100 { pageSize = 100 } offset := (page - 1) * pageSize objects, err := s.objectRepository.ListByOwner(ownerID, offset, pageSize) if err != nil { return nil, fmt.Errorf("failed to get objects by owner: %w", err) } total, _ := s.countObjectsByOwner(ownerID) items := make([]ObjectShortResponse, len(objects)) for i, obj := range objects { items[i] = s.mapToObjectShortResponse(&obj) } totalPages := int(total) / pageSize if int(total)%pageSize > 0 { totalPages++ } return &ObjectListResponse{ Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, }, nil } // GetObjectsByType возвращает объекты по типу func (s *objectServiceImpl) GetObjectsByType(ctx context.Context, objectType string, page, pageSize int) (*ObjectListResponse, error) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 10 } if pageSize > 100 { pageSize = 100 } offset := (page - 1) * pageSize objects, err := s.objectRepository.ListByType(objectType, offset, pageSize) if err != nil { return nil, fmt.Errorf("failed to get objects by type: %w", err) } total, _ := s.countObjectsByType(objectType) items := make([]ObjectShortResponse, len(objects)) for i, obj := range objects { items[i] = s.mapToObjectShortResponse(&obj) } totalPages := int(total) / pageSize if int(total)%pageSize > 0 { totalPages++ } return &ObjectListResponse{ Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, }, nil } // SearchObjects ищет объекты по запросу func (s *objectServiceImpl) SearchObjects(ctx context.Context, query string, page, pageSize int) (*ObjectListResponse, error) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 10 } if pageSize > 100 { pageSize = 100 } offset := (page - 1) * pageSize objects, err := s.objectRepository.Search(query, offset, pageSize) if err != nil { return nil, fmt.Errorf("failed to search objects: %w", err) } total, _ := s.countObjectsBySearch(query) items := make([]ObjectShortResponse, len(objects)) for i, obj := range objects { items[i] = s.mapToObjectShortResponse(&obj) } totalPages := int(total) / pageSize if int(total)%pageSize > 0 { totalPages++ } return &ObjectListResponse{ Items: items, Total: total, Page: page, PageSize: pageSize, TotalPages: totalPages, }, nil } // GetNearbyObjects возвращает объекты в радиусе func (s *objectServiceImpl) GetNearbyObjects(ctx context.Context, lat, lng, radius float64, page, pageSize int) (*ObjectListResponse, error) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 10 } if pageSize > 100 { pageSize = 100 } offset := (page - 1) * pageSize objects, err := s.objectRepository.GetNearby(lat, lng, radius, offset, pageSize) if err != nil { return nil, fmt.Errorf("failed to get nearby objects: %w", err) } items := make([]ObjectShortResponse, len(objects)) for i, obj := range objects { items[i] = s.mapToObjectShortResponse(&obj) } return &ObjectListResponse{ Items: items, Total: int64(len(objects)), Page: page, PageSize: pageSize, TotalPages: 1, }, nil } // ToggleVerification переключает статус верификации func (s *objectServiceImpl) ToggleVerification(ctx context.Context, id uint, verified bool) error { if err := s.objectRepository.ToggleVerification(id, verified); err != nil { return fmt.Errorf("failed to toggle verification: %w", err) } return nil } // CreateFeedback создает отзыв func (s *objectServiceImpl) CreateFeedback(ctx context.Context, req *CreateFeedbackRequest, ownerID uint) (*FeedbackResponse, error) { // Проверяем существование объекта if _, err := s.objectRepository.GetByID(req.ObjectID); err != nil { return nil, ErrObjectNotFound } feedback := &models.Feedback{ OwnerID: ownerID, ObjectID: req.ObjectID, Platform: req.Platform, Score: req.Score, Text: req.Text, } // TODO: Добавить метод CreateFeedback в репозиторий // if err := s.objectRepository.CreateFeedback(feedback); err != nil { // return nil, fmt.Errorf("failed to create feedback: %w", err) // } // Обновляем счетчик отзывов if err := s.objectRepository.UpdateFeedbackCount(req.ObjectID, 1); err != nil { // Логируем ошибку, но не прерываем выполнение fmt.Printf("Failed to update feedback count: %v\n", err) } return s.GetFeedbackByID(ctx, feedback.ID) } // UpdateFeedback обновляет отзыв func (s *objectServiceImpl) UpdateFeedback(ctx context.Context, id uint, req *UpdateFeedbackRequest, ownerID uint) (*FeedbackResponse, error) { // TODO: Реализовать обновление отзыва return nil, ErrNotImplemented } // DeleteFeedback удаляет отзыв func (s *objectServiceImpl) DeleteFeedback(ctx context.Context, id uint, ownerID uint) error { // TODO: Реализовать удаление отзыва return ErrNotImplemented } // GetFeedbackByID возвращает отзыв по ID func (s *objectServiceImpl) GetFeedbackByID(ctx context.Context, id uint) (*FeedbackResponse, error) { // TODO: Добавить метод GetFeedbackByID в репозиторий // feedback, err := s.objectRepository.GetFeedbackByID(id) // if err != nil { // return nil, fmt.Errorf("failed to get feedback: %w", err) // } // return s.mapToFeedbackResponse(feedback), nil return nil, ErrNotImplemented } // GetFeedbacksByObject возвращает отзывы объекта func (s *objectServiceImpl) GetFeedbacksByObject(ctx context.Context, objectID uint, page, pageSize int) (*FeedbackListResponse, error) { if page < 1 { page = 1 } if pageSize < 1 { pageSize = 10 } if pageSize > 100 { pageSize = 100 } offset := (page - 1) * pageSize feedbacks, err := s.objectRepository.GetFeedbacks(objectID, offset, pageSize) if err != nil { return nil, fmt.Errorf("failed to get feedbacks: %w", err) } count, err := s.objectRepository.GetFeedbackCount(objectID) if err != nil { count = 0 } items := make([]FeedbackResponse, len(feedbacks)) for i, fb := range feedbacks { items[i] = *s.mapToFeedbackResponse(&fb) } totalPages := count / pageSize if count%pageSize > 0 { totalPages++ } return &FeedbackListResponse{ Items: items, Total: int64(count), Page: page, PageSize: pageSize, TotalPages: totalPages, }, nil } // CreateRatingVote создает голос в рейтинге func (s *objectServiceImpl) CreateRatingVote(ctx context.Context, req *CreateRatingVoteRequest, voterID uint) (*RatingVoteResponse, error) { // Проверяем, не голосовал ли уже пользователь existing, _ := s.GetUserRatingVote(ctx, req.TargetID, voterID, req.Platform) if existing != nil { return nil, ErrAlreadyVoted } ratingVote := &models.RatingVote{ Platform: req.Platform, TargetID: req.TargetID, VoterID: voterID, Score: req.Score, } // TODO: Добавить метод CreateRatingVote в репозиторий // if err := s.objectRepository.CreateRatingVote(ratingVote); err != nil { // return nil, fmt.Errorf("failed to create rating vote: %w", err) // } // Обновляем статистику рейтинга s.updateRatingStats(req.TargetID, req.Platform) return &RatingVoteResponse{ ID: ratingVote.ID, CreatedAt: ratingVote.CreatedAt, Platform: ratingVote.Platform, TargetID: ratingVote.TargetID, VoterID: ratingVote.VoterID, Score: ratingVote.Score, }, nil } // GetObjectRating возвращает рейтинг объекта func (s *objectServiceImpl) GetObjectRating(ctx context.Context, objectID uint, platform models.PlatformType) (*RatingResponse, error) { var rating *models.Rating var err error if platform == models.PlatformTourist { rating, err = s.objectRepository.GetTouristRating(objectID) } else { rating, err = s.objectRepository.GetEntrepreneurRating(objectID) } if err != nil { return nil, fmt.Errorf("failed to get rating: %w", err) } return s.mapToRatingResponse(rating), nil } // GetUserRatingVote возвращает голос пользователя func (s *objectServiceImpl) GetUserRatingVote(ctx context.Context, objectID uint, userID uint, platform models.PlatformType) (*RatingVoteResponse, error) { // TODO: Добавить метод GetUserRatingVote в репозиторий // vote, err := s.objectRepository.GetUserRatingVote(objectID, userID, platform) // if err != nil { // return nil, err // } // return &RatingVoteResponse{ // ID: vote.ID, // CreatedAt: vote.CreatedAt, // Platform: vote.Platform, // TargetID: vote.TargetID, // VoterID: vote.VoterID, // Score: vote.Score, // }, nil return nil, ErrNotImplemented } // Вспомогательные методы func (s *objectServiceImpl) validateCreateRequest(req *CreateObjectRequest) error { if req.OwnerID == 0 { return ErrInvalidOwnerID } if req.ShortName == "" { return ErrShortNameRequired } return nil } func (s *objectServiceImpl) applyUpdates(object *models.Object, req *UpdateObjectRequest) { if req.Title != nil { object.Title = *req.Title } if req.ShortName != nil { object.ShortName = *req.ShortName } if req.LongName != nil { object.LongName = *req.LongName } if req.Type != nil { object.Type = *req.Type } if req.Price != nil { object.Price = *req.Price } if req.PricePeriod != nil { object.PricePeriod = *req.PricePeriod } if req.Phone != nil { object.Phone = *req.Phone } if req.Email != nil { object.Email = *req.Email } if req.Site != nil { object.Site = *req.Site } if req.ShortDescription != nil { object.ShortDescription = *req.ShortDescription } if req.Description != nil { object.Description = *req.Description } if req.Address != nil { object.Address = *req.Address } if req.Latitude != nil { object.Latitude = *req.Latitude } if req.Longitude != nil { object.Longitude = *req.Longitude } if req.Status != nil { object.Status = models.ObjectStatus(*req.Status) } if req.IsActive != nil { object.IsActive = *req.IsActive } if req.IsVerified != nil { object.IsVerified = *req.IsVerified } } func (s *objectServiceImpl) initializeRatings(objectID uint) { // Создаем записи рейтингов для туристической и предпринимательской платформ // TODO: Добавить создание рейтингов в репозиторий } func (s *objectServiceImpl) updateRatingStats(objectID uint, platform models.PlatformType) { // Обновляем статистику рейтинга // TODO: Реализовать обновление статистики } func (s *objectServiceImpl) mapToObjectResponse(object *models.Object, owner *models.Account, touristRating, entrepreneurRating *models.Rating, feedbacks []models.Feedback) *ObjectResponse { resp := &ObjectResponse{ ID: object.ID, CreatedAt: object.CreatedAt, UpdatedAt: object.UpdatedAt, OwnerID: object.OwnerID, Title: object.Title, ShortName: object.ShortName, LongName: object.LongName, Type: object.Type, Price: object.Price, PricePeriod: object.PricePeriod, Phone: object.Phone, Email: object.Email, Site: object.Site, ShortDescription: object.ShortDescription, Description: object.Description, Address: object.Address, Latitude: object.Latitude, Longitude: object.Longitude, IsActive: object.IsActive, IsVerified: object.IsVerified, Status: string(object.Status), ViewCount: object.ViewCount, FeedbackCount: object.FeedbackCount, } if object.DeletedAt.Valid { resp.DeletedAt = &object.DeletedAt.Time } if owner != nil { resp.Owner = &account.AccountResponse{ ID: owner.ID, FullName: owner.FullName, Email: owner.Email, // Добавьте другие поля } } if touristRating != nil { resp.TouristRating = s.mapToRatingResponse(touristRating) } if entrepreneurRating != nil { resp.EntrepreneurRating = s.mapToRatingResponse(entrepreneurRating) } if len(feedbacks) > 0 { resp.Feedbacks = make([]FeedbackShortResponse, len(feedbacks)) for i, fb := range feedbacks { resp.Feedbacks[i] = FeedbackShortResponse{ ID: fb.ID, CreatedAt: fb.CreatedAt, OwnerID: fb.OwnerID, Platform: fb.Platform, Score: fb.Score, Text: fb.Text, } } } if len(object.Images) > 0 { resp.Images = make([]ImageResponse, len(object.Images)) for i, img := range object.Images { resp.Images[i] = ImageResponse{ ID: img.ID, ObjectID: img.ObjectID, URL: img.URL, IsPrimary: img.IsPrimary, SortOrder: img.SortOrder, } } } if len(object.Amenities) > 0 { resp.Amenities = make([]AmenityResponse, len(object.Amenities)) for i, a := range object.Amenities { resp.Amenities[i] = AmenityResponse{ ID: a.ID, Name: a.Name, Category: a.Category, Icon: a.Icon, Description: a.Description, } } } return resp } func (s *objectServiceImpl) mapToObjectShortResponse(object *models.Object) ObjectShortResponse { return ObjectShortResponse{ ID: object.ID, Title: object.Title, ShortName: object.ShortName, LongName: object.LongName, Type: object.Type, Price: object.Price, PricePeriod: object.PricePeriod, Address: object.Address, IsActive: object.IsActive, IsVerified: object.IsVerified, Status: string(object.Status), FeedbackCount: object.FeedbackCount, } } func (s *objectServiceImpl) mapToRatingResponse(rating *models.Rating) *RatingResponse { return &RatingResponse{ ID: rating.ID, CreatedAt: rating.CreatedAt, UpdatedAt: rating.UpdatedAt, Platform: rating.Platform, AverageScore: rating.AverageScore, TotalVotes: rating.TotalVotes, VoteBreakdown: VoteBreakdownDTO{ Score1: rating.VoteBreakdown.Score1, Score2: rating.VoteBreakdown.Score2, Score3: rating.VoteBreakdown.Score3, Score4: rating.VoteBreakdown.Score4, Score5: rating.VoteBreakdown.Score5, }, } } func (s *objectServiceImpl) mapToFeedbackResponse(feedback *models.Feedback) *FeedbackResponse { return &FeedbackResponse{ ID: feedback.ID, CreatedAt: feedback.CreatedAt, UpdatedAt: feedback.UpdatedAt, OwnerID: feedback.OwnerID, ObjectID: feedback.ObjectID, Platform: feedback.Platform, Score: feedback.Score, Text: feedback.Text, CommentCount: feedback.CommentCount, } } // Методы для подсчета (временные, должны быть в репозитории) func (s *objectServiceImpl) countObjectsByType(objectType string) (int64, error) { // TODO: Добавить метод CountByType в репозиторий return 0, nil } func (s *objectServiceImpl) countObjectsByStatus(isActive bool) (int64, error) { // TODO: Добавить метод CountByStatus в репозиторий return 0, nil } func (s *objectServiceImpl) countObjectsByOwner(ownerID uint) (int64, error) { // TODO: Добавить метод CountByOwner в репозиторий return 0, nil } func (s *objectServiceImpl) countObjectsBySearch(query string) (int64, error) { // TODO: Добавить метод CountBySearch в репозиторий return 0, nil } func (s *objectServiceImpl) countObjectsByStatusString(status string) (int64, error) { return 0, nil }