diff --git a/main_dc/yalarba/api_es/internal/repository/object_repository.go b/main_dc/yalarba/api_es/internal/repository/object_repository.go new file mode 100644 index 0000000..6ea5910 --- /dev/null +++ b/main_dc/yalarba/api_es/internal/repository/object_repository.go @@ -0,0 +1,398 @@ +package repository + +import ( + "errors" + "gorm.io/gorm" + "api_es/internal/models" +) + +var ( + ErrObjectNotFound = errors.New("object not found") +) + +type ObjectRepository interface { + // Основные операции + Create(object *models.Object) error + GetByID(id uint) (*models.Object, error) + Update(id uint, updates *models.ObjectUpdateRequest) error + Delete(id uint) error + List(filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error) + + // Специфичные операции + GetByOwner(ownerID uint, filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error) + UpdateStatus(id uint, status models.ObjectStatus) error + IncrementViewCount(id uint) error + UpdateRating(id uint, rating float64, reviewCount int) error + + // Работа с изображениями + AddImage(objectID uint, image *models.ObjectImage) error + RemoveImage(objectID uint, imageID uint) error + SetPrimaryImage(objectID uint, imageID uint) error + GetImages(objectID uint) ([]models.ObjectImage, error) + + // Работа с удобствами + AddAmenities(objectID uint, amenityIDs []uint) error + RemoveAmenities(objectID uint, amenityIDs []uint) error + GetAmenities(objectID uint) ([]models.Amenity, error) +} + +type ObjectFilter struct { + Type []models.ObjectType + City string + Status []models.ObjectStatus + OwnerID uint + MinPrice float64 + MaxPrice float64 + MinRating float64 + AmenityIDs []uint + Search string +} + +type Pagination struct { + Page int `form:"page" default:"1"` + PageSize int `form:"page_size" default:"20"` +} + +type objectRepository struct { + db *gorm.DB +} + +func NewObjectRepository(db *gorm.DB) ObjectRepository { + return &objectRepository{db: db} +} + +// Create создает новый объект +func (r *objectRepository) Create(object *models.Object) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // Создаем основной объект + if err := tx.Create(object).Error; err != nil { + return err + } + + // Добавляем связи с удобствами, если они есть + if len(object.Amenities) > 0 { + if err := tx.Model(object).Association("Amenities").Append(object.Amenities); err != nil { + return err + } + } + + return nil + }) +} + +// GetByID возвращает объект по ID с связанными данными +func (r *objectRepository) GetByID(id uint) (*models.Object, error) { + var object models.Object + err := r.db. + Preload("Owner", func(db *gorm.DB) *gorm.DB { + return db.Select("id, first_name, last_name, email, phone") + }). + Preload("Images", func(db *gorm.DB) *gorm.DB { + return db.Order("is_primary DESC, order ASC") + }). + Preload("Amenities"). + First(&object, id).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrObjectNotFound + } + return nil, err + } + + return &object, nil +} + +// Update обновляет объект +func (r *objectRepository) Update(id uint, updates *models.ObjectUpdateRequest) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // Обновляем основные поля + updateData := map[string]interface{}{} + + if updates.Title != "" { + updateData["title"] = updates.Title + } + if updates.Type != "" { + updateData["type"] = updates.Type + } + if updates.Description != "" { + updateData["description"] = updates.Description + } + if updates.City != "" { + updateData["city"] = updates.City + } + if updates.Address != "" { + updateData["address"] = updates.Address + } + if updates.Latitude != 0 { + updateData["latitude"] = updates.Latitude + } + if updates.Longitude != 0 { + updateData["longitude"] = updates.Longitude + } + if updates.Price != 0 { + updateData["price"] = updates.Price + } + if updates.PricePeriod != "" { + updateData["price_period"] = updates.PricePeriod + } + if updates.Status != "" { + updateData["status"] = updates.Status + } + + if len(updateData) > 0 { + if err := tx.Model(&models.Object{}).Where("id = ?", id).Updates(updateData).Error; err != nil { + return err + } + } + + // Обновляем удобства, если переданы + if updates.AmenityIDs != nil { + var object models.Object + if err := tx.First(&object, id).Error; err != nil { + return err + } + + var amenities []models.Amenity + if err := tx.Where("id IN ?", updates.AmenityIDs).Find(&amenities).Error; err != nil { + return err + } + + if err := tx.Model(&object).Association("Amenities").Replace(amenities); err != nil { + return err + } + } + + return nil + }) +} + +// Delete удаляет объект (мягкое удаление) +func (r *objectRepository) Delete(id uint) error { + result := r.db.Delete(&models.Object{}, id) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrObjectNotFound + } + return nil +} + +// List возвращает список объектов с фильтрацией и пагинацией +func (r *objectRepository) List(filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error) { + var objects []models.Object + var total int64 + + query := r.db.Model(&models.Object{}) + + // Применяем фильтры + if filter != nil { + query = r.applyFilters(query, filter) + } + + // Считаем общее количество + if err := query.Count(&total).Error; err != nil { + return nil, 0, err + } + + // Применяем пагинацию + if pagination != nil { + offset := (pagination.Page - 1) * pagination.PageSize + query = query.Offset(offset).Limit(pagination.PageSize) + } + + // Загружаем данные с прелоадами + err := query. + Preload("Images", func(db *gorm.DB) *gorm.DB { + return db.Where("is_primary = ?", true).Limit(1) + }). + Preload("Amenities"). + Order("created_at DESC"). + Find(&objects).Error + + if err != nil { + return nil, 0, err + } + + return objects, total, nil +} + +// GetByOwner возвращает объекты владельца +func (r *objectRepository) GetByOwner(ownerID uint, filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error) { + if filter == nil { + filter = &ObjectFilter{} + } + filter.OwnerID = ownerID + return r.List(filter, pagination) +} + +// UpdateStatus обновляет статус объекта +func (r *objectRepository) UpdateStatus(id uint, status models.ObjectStatus) error { + result := r.db.Model(&models.Object{}).Where("id = ?", id).Update("status", status) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrObjectNotFound + } + return nil +} + +// IncrementViewCount увеличивает счетчик просмотров +func (r *objectRepository) IncrementViewCount(id uint) error { + return r.db.Model(&models.Object{}). + Where("id = ?", id). + Update("view_count", gorm.Expr("view_count + ?", 1)). + Error +} + +// UpdateRating обновляет рейтинг и количество отзывов +func (r *objectRepository) UpdateRating(id uint, rating float64, reviewCount int) error { + return r.db.Model(&models.Object{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "rating": rating, + "review_count": reviewCount, + }).Error +} + +// AddImage добавляет изображение к объекту +func (r *objectRepository) AddImage(objectID uint, image *models.ObjectImage) error { + image.ObjectID = objectID + return r.db.Create(image).Error +} + +// RemoveImage удаляет изображение объекта +func (r *objectRepository) RemoveImage(objectID uint, imageID uint) error { + result := r.db.Where("object_id = ? AND id = ?", objectID, imageID).Delete(&models.ObjectImage{}) + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrObjectNotFound + } + return nil +} + +// SetPrimaryImage устанавливает основное изображение +func (r *objectRepository) SetPrimaryImage(objectID uint, imageID uint) error { + return r.db.Transaction(func(tx *gorm.DB) error { + // Сбрасываем все is_primary для объекта + if err := tx.Model(&models.ObjectImage{}). + Where("object_id = ?", objectID). + Update("is_primary", false).Error; err != nil { + return err + } + + // Устанавливаем новое основное изображение + result := tx.Model(&models.ObjectImage{}). + Where("object_id = ? AND id = ?", objectID, imageID). + Update("is_primary", true) + + if result.Error != nil { + return result.Error + } + if result.RowsAffected == 0 { + return ErrObjectNotFound + } + + return nil + }) +} + +// GetImages возвращает изображения объекта +func (r *objectRepository) GetImages(objectID uint) ([]models.ObjectImage, error) { + var images []models.ObjectImage + err := r.db.Where("object_id = ?", objectID). + Order("is_primary DESC, order ASC"). + Find(&images).Error + return images, err +} + +// AddAmenities добавляет удобства к объекту +func (r *objectRepository) AddAmenities(objectID uint, amenityIDs []uint) error { + return r.db.Transaction(func(tx *gorm.DB) error { + var object models.Object + if err := tx.First(&object, objectID).Error; err != nil { + return err + } + + var amenities []models.Amenity + if err := tx.Where("id IN ?", amenityIDs).Find(&amenities).Error; err != nil { + return err + } + + return tx.Model(&object).Association("Amenities").Append(amenities) + }) +} + +// RemoveAmenities удаляет удобства у объекта +func (r *objectRepository) RemoveAmenities(objectID uint, amenityIDs []uint) error { + return r.db.Transaction(func(tx *gorm.DB) error { + var object models.Object + if err := tx.First(&object, objectID).Error; err != nil { + return err + } + + var amenities []models.Amenity + if err := tx.Where("id IN ?", amenityIDs).Find(&amenities).Error; err != nil { + return err + } + + return tx.Model(&object).Association("Amenities").Delete(amenities) + }) +} + +// GetAmenities возвращает удобства объекта +func (r *objectRepository) GetAmenities(objectID uint) ([]models.Amenity, error) { + var amenities []models.Amenity + err := r.db.Joins("JOIN object_amenities ON amenities.id = object_amenities.amenity_id"). + Where("object_amenities.object_id = ?", objectID). + Find(&amenities).Error + return amenities, err +} + +// applyFilters применяет фильтры к запросу +func (r *objectRepository) applyFilters(query *gorm.DB, filter *ObjectFilter) *gorm.DB { + if len(filter.Type) > 0 { + query = query.Where("type IN ?", filter.Type) + } + + if filter.City != "" { + query = query.Where("city = ?", filter.City) + } + + if len(filter.Status) > 0 { + query = query.Where("status IN ?", filter.Status) + } + + if filter.OwnerID != 0 { + query = query.Where("owner_id = ?", filter.OwnerID) + } + + if filter.MinPrice > 0 { + query = query.Where("price >= ?", filter.MinPrice) + } + + if filter.MaxPrice > 0 { + query = query.Where("price <= ?", filter.MaxPrice) + } + + if filter.MinRating > 0 { + query = query.Where("rating >= ?", filter.MinRating) + } + + if filter.Search != "" { + search := "%" + filter.Search + "%" + query = query.Where("title ILIKE ? OR description ILIKE ?", search, search) + } + + // Фильтр по удобствам + if len(filter.AmenityIDs) > 0 { + query = query.Joins("JOIN object_amenities ON objects.id = object_amenities.object_id"). + Where("object_amenities.amenity_id IN ?", filter.AmenityIDs) + } + + return query +} \ No newline at end of file