From 979c265e36ed5458de4a5910b64026b46056e193 Mon Sep 17 00:00:00 2001 From: valitovgaziz Date: Tue, 31 Mar 2026 16:53:24 +0500 Subject: [PATCH] On branch main modified: main_dc/yalarba/api_yal/internal/domain/account/handler.go modified: main_dc/yalarba/api_yal/internal/domain/account/router.go modified: main_dc/yalarba/api_yal/internal/domain/account/service.go modified: main_dc/yalarba/api_yal/internal/domain/auth/router.go new file: main_dc/yalarba/api_yal/internal/domain/comment/dto.go new file: main_dc/yalarba/api_yal/internal/domain/feetback/dto.go new file: main_dc/yalarba/api_yal/internal/domain/object/dto.go new file: main_dc/yalarba/api_yal/internal/domain/object/errors.go new file: main_dc/yalarba/api_yal/internal/domain/object/handler.go new file: main_dc/yalarba/api_yal/internal/domain/object/router.go new file: main_dc/yalarba/api_yal/internal/domain/object/service.go new file: main_dc/yalarba/api_yal/internal/domain/object/types.go new file: main_dc/yalarba/api_yal/internal/domain/rating/dto.go modified: main_dc/yalarba/api_yal/internal/models/rating.go add and not tested Object's domain --- .../internal/domain/account/handler.go | 104 +-- .../api_yal/internal/domain/account/router.go | 6 - .../internal/domain/account/service.go | 97 +-- .../api_yal/internal/domain/auth/router.go | 3 +- .../api_yal/internal/domain/comment/dto.go | 25 + .../api_yal/internal/domain/feetback/dto.go | 6 + .../api_yal/internal/domain/object/dto.go | 179 +++++ .../api_yal/internal/domain/object/errors.go | 11 + .../api_yal/internal/domain/object/handler.go | 296 +++++++ .../api_yal/internal/domain/object/router.go | 47 ++ .../api_yal/internal/domain/object/service.go | 731 ++++++++++++++++++ .../api_yal/internal/domain/object/types.go | 19 + .../api_yal/internal/domain/rating/dto.go | 6 + .../yalarba/api_yal/internal/models/rating.go | 4 +- 14 files changed, 1392 insertions(+), 142 deletions(-) create mode 100644 main_dc/yalarba/api_yal/internal/domain/comment/dto.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/feetback/dto.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/object/dto.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/object/errors.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/object/handler.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/object/router.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/object/service.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/object/types.go create mode 100644 main_dc/yalarba/api_yal/internal/domain/rating/dto.go diff --git a/main_dc/yalarba/api_yal/internal/domain/account/handler.go b/main_dc/yalarba/api_yal/internal/domain/account/handler.go index 1058058..b4c7618 100644 --- a/main_dc/yalarba/api_yal/internal/domain/account/handler.go +++ b/main_dc/yalarba/api_yal/internal/domain/account/handler.go @@ -14,22 +14,22 @@ import ( "go.uber.org/zap" ) -// Handler обработчик для операций с аккаунтами -type Handler struct { - service Service +// AccountHandler обработчик для операций с аккаунтами +type AccountHandler struct { + service AccountService validator *validator.Validate } // NewHandler создает новый экземпляр Handler -func NewHandler(service Service) *Handler { - return &Handler{ +func NewHandler(service AccountService) *AccountHandler { + return &AccountHandler{ service: service, validator: validator.New(), } } // GetAccountByID получение аккаунта по ID -func (h *Handler) GetAccountByID(w http.ResponseWriter, r *http.Request) { +func (h *AccountHandler) GetAccountByID(w http.ResponseWriter, r *http.Request) { l := logger.Get() // Получаем ID из контекста (для своего профиля) @@ -59,7 +59,7 @@ func (h *Handler) GetAccountByID(w http.ResponseWriter, r *http.Request) { } // GetAccountProfile получение профиля пользователя -func (h *Handler) GetAccountProfile(w http.ResponseWriter, r *http.Request) { +func (h *AccountHandler) GetAccountProfile(w http.ResponseWriter, r *http.Request) { l := logger.Get() userID, ok := r.Context().Value(middleware.UserIDKey).(uint) @@ -86,7 +86,7 @@ func (h *Handler) GetAccountProfile(w http.ResponseWriter, r *http.Request) { } // UpdateAccount обновление аккаунта -func (h *Handler) UpdateAccount(w http.ResponseWriter, r *http.Request) { +func (h *AccountHandler) UpdateAccount(w http.ResponseWriter, r *http.Request) { l := logger.Get() userID, ok := r.Context().Value(middleware.UserIDKey).(uint) @@ -120,7 +120,7 @@ func (h *Handler) UpdateAccount(w http.ResponseWriter, r *http.Request) { } // ChangePassword смена пароля -func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) { +func (h *AccountHandler) ChangePassword(w http.ResponseWriter, r *http.Request) { l := logger.Get() userID, ok := r.Context().Value(middleware.UserIDKey).(uint) @@ -163,82 +163,8 @@ func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) { }) } -// ForgotPassword запрос на сброс пароля -func (h *Handler) ForgotPassword(w http.ResponseWriter, r *http.Request) { - l := logger.Get() - - var req ForgotPasswordRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - if err := h.validator.Struct(req); err != nil { - h.handleValidationError(w, err) - return - } - - token, err := h.service.ForgotPassword(req.Email) - if err != nil { - l.Error("Ошибка запроса сброса пароля", zap.Error(err)) - http.Error(w, "Password reset request failed", http.StatusInternalServerError) - return - } - - // В реальном приложении здесь отправляется email - // Для тестирования возвращаем токен - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(PasswordResetResponse{ - Message: "If the email exists, a reset link has been sent", - Token: token, // Только для тестирования - }) -} - -// ResetPassword подтверждение сброса пароля -func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) { - l := logger.Get() - - var req ResetPasswordRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, "Invalid request body", http.StatusBadRequest) - return - } - - if err := h.validator.Struct(req); err != nil { - h.handleValidationError(w, err) - return - } - - if err := h.service.ResetPassword(req.Token, req.NewPassword); err != nil { - l.Error("Ошибка сброса пароля", zap.Error(err)) - - if errors.Is(err, ErrResetTokenNotFound) { - http.Error(w, "Reset token not found", http.StatusNotFound) - return - } - - if errors.Is(err, ErrResetTokenExpired) { - http.Error(w, "Reset token has expired", http.StatusBadRequest) - return - } - - if errors.Is(err, ErrResetTokenAlreadyUsed) { - http.Error(w, "Reset token has already been used", http.StatusBadRequest) - return - } - - http.Error(w, "Password reset failed", http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(map[string]string{ - "message": "Password has been successfully reset", - }) -} - // DeleteAccount удаление аккаунта -func (h *Handler) DeleteAccount(w http.ResponseWriter, r *http.Request) { +func (h *AccountHandler) DeleteAccount(w http.ResponseWriter, r *http.Request) { l := logger.Get() userID, ok := r.Context().Value(middleware.UserIDKey).(uint) @@ -268,7 +194,7 @@ func (h *Handler) DeleteAccount(w http.ResponseWriter, r *http.Request) { // ==================== Административные методы ==================== // ListAccounts список аккаунтов (админ) -func (h *Handler) ListAccounts(w http.ResponseWriter, r *http.Request) { +func (h *AccountHandler) ListAccounts(w http.ResponseWriter, r *http.Request) { l := logger.Get() var req ListAccountsRequest @@ -306,7 +232,7 @@ func (h *Handler) ListAccounts(w http.ResponseWriter, r *http.Request) { } // GetAccountByIDAdmin получение аккаунта по ID (админ) -func (h *Handler) GetAccountByIDAdmin(w http.ResponseWriter, r *http.Request) { +func (h *AccountHandler) GetAccountByIDAdmin(w http.ResponseWriter, r *http.Request) { l := logger.Get() // Получаем ID из URL @@ -340,7 +266,7 @@ func (h *Handler) GetAccountByIDAdmin(w http.ResponseWriter, r *http.Request) { } // VerifyAccount верификация аккаунта (админ) -func (h *Handler) VerifyAccount(w http.ResponseWriter, r *http.Request) { +func (h *AccountHandler) VerifyAccount(w http.ResponseWriter, r *http.Request) { l := logger.Get() // Получаем ID из URL @@ -381,7 +307,7 @@ func (h *Handler) VerifyAccount(w http.ResponseWriter, r *http.Request) { } // UpdateAccountStatus обновление статуса аккаунта (админ) -func (h *Handler) UpdateAccountStatus(w http.ResponseWriter, r *http.Request) { +func (h *AccountHandler) UpdateAccountStatus(w http.ResponseWriter, r *http.Request) { l := logger.Get() // Получаем ID из URL @@ -427,7 +353,7 @@ func (h *Handler) UpdateAccountStatus(w http.ResponseWriter, r *http.Request) { } // handleValidationError обрабатывает ошибки валидации -func (h *Handler) handleValidationError(w http.ResponseWriter, err error) { +func (h *AccountHandler) handleValidationError(w http.ResponseWriter, err error) { var invalidValidationError *validator.InvalidValidationError if errors.As(err, &invalidValidationError) { http.Error(w, "Invalid request", http.StatusBadRequest) diff --git a/main_dc/yalarba/api_yal/internal/domain/account/router.go b/main_dc/yalarba/api_yal/internal/domain/account/router.go index 4135393..078ba62 100644 --- a/main_dc/yalarba/api_yal/internal/domain/account/router.go +++ b/main_dc/yalarba/api_yal/internal/domain/account/router.go @@ -19,12 +19,6 @@ func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { accountService := NewService(accountRepo) accountHandler := NewHandler(accountService) - // Публичные маршруты (без аутентификации) - r.Group(func(r chi.Router) { - r.Post("/forgot-password", accountHandler.ForgotPassword) - r.Post("/reset-password", accountHandler.ResetPassword) - }) - // Защищенные маршруты (требуют аутентификации) r.Group(func(r chi.Router) { r.Use(middleware.AuthMiddleware(jwtSecret)) diff --git a/main_dc/yalarba/api_yal/internal/domain/account/service.go b/main_dc/yalarba/api_yal/internal/domain/account/service.go index 83a4dce..7647ce6 100644 --- a/main_dc/yalarba/api_yal/internal/domain/account/service.go +++ b/main_dc/yalarba/api_yal/internal/domain/account/service.go @@ -4,6 +4,8 @@ import ( "api_yal/internal/logger" "api_yal/internal/models" "api_yal/internal/repository" + "crypto/rand" + "encoding/hex" "errors" "fmt" "time" @@ -13,29 +15,27 @@ import ( "gorm.io/gorm" ) -// Service интерфейс сервиса аккаунтов -type Service interface { +// AccountService интерфейс сервиса аккаунтов +type AccountService interface { // Основные операции GetAccountByID(id uint) (*AccountResponse, error) GetAccountByEmail(email string) (*AccountResponse, error) GetAccountWithObjects(id uint) (*AccountWithObjectsResponse, error) UpdateAccount(id uint, req UpdateAccountRequest) (*AccountResponse, error) DeleteAccount(id uint) error - + // Управление паролем ChangePassword(userID uint, req ChangePasswordRequest) error - ForgotPassword(email string) (string, error) - ResetPassword(token, newPassword string) error - + // Административные функции ListAccounts(req ListAccountsRequest) (*AccountListResponse, error) VerifyAccount(accountID uint, req VerifyAccountRequest) error UpdateAccountStatus(accountID uint, req UpdateAccountStatusRequest) error - + // Статистика GetAccountStats(userID uint) (*AccountStats, error) GetAccountProfile(userID uint) (*AccountProfileResponse, error) - + // Внутренние методы для auth сервиса GetAccountModelByID(id uint) (*models.Account, error) GetAccountModelByEmail(email string) (*models.Account, error) @@ -43,20 +43,20 @@ type Service interface { UpdateAccountModel(account *models.Account) error } -// serviceImpl реализация сервиса аккаунтов -type serviceImpl struct { +// accountServiceImpl реализация сервиса аккаунтов +type accountServiceImpl struct { accountRepo repository.AccountRepository } // NewService создает новый экземпляр сервиса аккаунтов -func NewService(accountRepo repository.AccountRepository) Service { - return &serviceImpl{ +func NewService(accountRepo repository.AccountRepository) AccountService { + return &accountServiceImpl{ accountRepo: accountRepo, } } // GetAccountByID получает аккаунт по ID -func (s *serviceImpl) GetAccountByID(id uint) (*AccountResponse, error) { +func (s *accountServiceImpl) GetAccountByID(id uint) (*AccountResponse, error) { l := logger.Get() l.Debug("Получение аккаунта по ID", zap.Uint("id", id)) @@ -74,7 +74,7 @@ func (s *serviceImpl) GetAccountByID(id uint) (*AccountResponse, error) { } // GetAccountByEmail получает аккаунт по email -func (s *serviceImpl) GetAccountByEmail(email string) (*AccountResponse, error) { +func (s *accountServiceImpl) GetAccountByEmail(email string) (*AccountResponse, error) { l := logger.Get() l.Debug("Получение аккаунта по email", zap.String("email", email)) @@ -92,7 +92,7 @@ func (s *serviceImpl) GetAccountByEmail(email string) (*AccountResponse, error) } // GetAccountWithObjects получает аккаунт с его объектами -func (s *serviceImpl) GetAccountWithObjects(id uint) (*AccountWithObjectsResponse, error) { +func (s *accountServiceImpl) GetAccountWithObjects(id uint) (*AccountWithObjectsResponse, error) { l := logger.Get() l.Debug("Получение аккаунта с объектами", zap.Uint("id", id)) @@ -117,7 +117,7 @@ func (s *serviceImpl) GetAccountWithObjects(id uint) (*AccountWithObjectsRespons } // UpdateAccount обновляет информацию об аккаунте -func (s *serviceImpl) UpdateAccount(id uint, req UpdateAccountRequest) (*AccountResponse, error) { +func (s *accountServiceImpl) UpdateAccount(id uint, req UpdateAccountRequest) (*AccountResponse, error) { l := logger.Get() l.Info("Обновление аккаунта", zap.Uint("id", id)) @@ -171,7 +171,7 @@ func (s *serviceImpl) UpdateAccount(id uint, req UpdateAccountRequest) (*Account } // DeleteAccount удаляет аккаунт (мягкое удаление) -func (s *serviceImpl) DeleteAccount(id uint) error { +func (s *accountServiceImpl) DeleteAccount(id uint) error { l := logger.Get() l.Info("Удаление аккаунта", zap.Uint("id", id)) @@ -184,7 +184,7 @@ func (s *serviceImpl) DeleteAccount(id uint) error { } // ChangePassword изменяет пароль пользователя -func (s *serviceImpl) ChangePassword(userID uint, req ChangePasswordRequest) error { +func (s *accountServiceImpl) ChangePassword(userID uint, req ChangePasswordRequest) error { l := logger.Get() l.Info("Смена пароля", zap.Uint("userID", userID)) @@ -220,7 +220,7 @@ func (s *serviceImpl) ChangePassword(userID uint, req ChangePasswordRequest) err } // ForgotPassword запрашивает сброс пароля -func (s *serviceImpl) ForgotPassword(email string) (string, error) { +func (s *accountServiceImpl) ForgotPassword(email string) (string, error) { l := logger.Get() l.Info("Запрос сброса пароля", zap.String("email", email)) @@ -235,7 +235,11 @@ func (s *serviceImpl) ForgotPassword(email string) (string, error) { // Генерируем reset token (используем метод из auth сервиса) // В реальном приложении здесь должна быть генерация токена - resetToken := generateResetToken() + resetToken, err := generateResetToken() + if err != nil { + l.Error("Ошибка генерации reset token", zap.Error(err)) + return "", err + } passwordReset := &models.PasswordReset{ AccountID: account.ID, @@ -254,7 +258,7 @@ func (s *serviceImpl) ForgotPassword(email string) (string, error) { } // ResetPassword сбрасывает пароль по токену -func (s *serviceImpl) ResetPassword(token, newPassword string) error { +func (s *accountServiceImpl) ResetPassword(token, newPassword string) error { l := logger.Get() l.Info("Сброс пароля по токену") @@ -307,7 +311,7 @@ func (s *serviceImpl) ResetPassword(token, newPassword string) error { } // ListAccounts возвращает список аккаунтов с пагинацией -func (s *serviceImpl) ListAccounts(req ListAccountsRequest) (*AccountListResponse, error) { +func (s *accountServiceImpl) ListAccounts(req ListAccountsRequest) (*AccountListResponse, error) { l := logger.Get() l.Debug("Получение списка аккаунтов", zap.Any("request", req)) @@ -320,7 +324,7 @@ func (s *serviceImpl) ListAccounts(req ListAccountsRequest) (*AccountListRespons } offset := (req.Page - 1) * req.PageSize - + var accounts []models.Account var total int64 var err error @@ -341,14 +345,14 @@ func (s *serviceImpl) ListAccounts(req ListAccountsRequest) (*AccountListRespons } total, err = s.accountRepo.Count() } - + if err != nil { return nil, err } // Фильтруем по роли и статусу (если нужно) filteredAccounts := s.filterAccounts(accounts, req.Role, req.IsActive) - + items := make([]AccountResponse, len(filteredAccounts)) for i, acc := range filteredAccounts { items[i] = ToAccountResponse(&acc) @@ -369,7 +373,7 @@ func (s *serviceImpl) ListAccounts(req ListAccountsRequest) (*AccountListRespons } // VerifyAccount верифицирует аккаунт -func (s *serviceImpl) VerifyAccount(accountID uint, req VerifyAccountRequest) error { +func (s *accountServiceImpl) VerifyAccount(accountID uint, req VerifyAccountRequest) error { l := logger.Get() l.Info("Верификация аккаунта", zap.Uint("id", accountID)) @@ -392,7 +396,7 @@ func (s *serviceImpl) VerifyAccount(accountID uint, req VerifyAccountRequest) er } // UpdateAccountStatus обновляет статус аккаунта (админ) -func (s *serviceImpl) UpdateAccountStatus(accountID uint, req UpdateAccountStatusRequest) error { +func (s *accountServiceImpl) UpdateAccountStatus(accountID uint, req UpdateAccountStatusRequest) error { l := logger.Get() l.Info("Обновление статуса аккаунта", zap.Uint("id", accountID)) @@ -419,13 +423,13 @@ func (s *serviceImpl) UpdateAccountStatus(accountID uint, req UpdateAccountStatu } // GetAccountStats получает статистику аккаунта -func (s *serviceImpl) GetAccountStats(userID uint) (*AccountStats, error) { +func (s *accountServiceImpl) GetAccountStats(userID uint) (*AccountStats, error) { l := logger.Get() l.Debug("Получение статистики аккаунта", zap.Uint("userID", userID)) // Здесь должна быть реальная логика подсчета статистики // В реальном приложении нужно запрашивать данные из соответствующих репозиториев - + stats := &AccountStats{ ObjectsCount: 0, FeedbacksCount: 0, @@ -433,14 +437,14 @@ func (s *serviceImpl) GetAccountStats(userID uint) (*AccountStats, error) { RatingsCount: 0, AppealsCount: 0, } - + // TODO: Получить реальную статистику из базы данных - + return stats, nil } // GetAccountProfile получает профиль пользователя со статистикой -func (s *serviceImpl) GetAccountProfile(userID uint) (*AccountProfileResponse, error) { +func (s *accountServiceImpl) GetAccountProfile(userID uint) (*AccountProfileResponse, error) { l := logger.Get() l.Debug("Получение профиля пользователя", zap.Uint("userID", userID)) @@ -465,17 +469,17 @@ func (s *serviceImpl) GetAccountProfile(userID uint) (*AccountProfileResponse, e } // GetAccountModelByID получает модель аккаунта по ID (для внутреннего использования) -func (s *serviceImpl) GetAccountModelByID(id uint) (*models.Account, error) { +func (s *accountServiceImpl) GetAccountModelByID(id uint) (*models.Account, error) { return s.accountRepo.GetByID(id) } // GetAccountModelByEmail получает модель аккаунта по email (для внутреннего использования) -func (s *serviceImpl) GetAccountModelByEmail(email string) (*models.Account, error) { +func (s *accountServiceImpl) GetAccountModelByEmail(email string) (*models.Account, error) { return s.accountRepo.GetByEmail(email) } // CreateAccount создает новый аккаунт -func (s *serviceImpl) CreateAccount(req CreateAccountRequest) (*models.Account, error) { +func (s *accountServiceImpl) CreateAccount(req CreateAccountRequest) (*models.Account, error) { l := logger.Get() l.Info("Создание аккаунта", zap.String("email", req.Email)) @@ -526,21 +530,21 @@ func (s *serviceImpl) CreateAccount(req CreateAccountRequest) (*models.Account, } // UpdateAccountModel обновляет модель аккаунта -func (s *serviceImpl) UpdateAccountModel(account *models.Account) error { +func (s *accountServiceImpl) UpdateAccountModel(account *models.Account) error { return s.accountRepo.Update(account) } // Вспомогательные методы -func (s *serviceImpl) getSearchTotal(query string) (int64, error) { +func (s *accountServiceImpl) getSearchTotal(query string) (int64, error) { // Здесь должна быть реализация подсчета общего количества результатов поиска // Для простоты возвращаем 0 return 0, nil } -func (s *serviceImpl) filterAccounts(accounts []models.Account, role string, isActive *bool) []models.Account { +func (s *accountServiceImpl) filterAccounts(accounts []models.Account, role string, isActive *bool) []models.Account { var filtered []models.Account - + for _, acc := range accounts { if role != "" && acc.Role != role { continue @@ -550,12 +554,17 @@ func (s *serviceImpl) filterAccounts(accounts []models.Account, role string, isA } filtered = append(filtered, acc) } - + return filtered } // Генерация reset токена -func generateResetToken() string { - // В реальном приложении используйте криптографически безопасную генерацию - return fmt.Sprintf("reset_%d_%d", time.Now().UnixNano(), time.Now().Unix()) -} \ No newline at end of file +func generateResetToken() (string, error) { + // Генерируем 32 байта (64 символа в hex) — достаточно для безопасности + bytes := make([]byte, 32) + _, err := rand.Read(bytes) + if err != nil { + return "", fmt.Errorf("failed to generate token: %w", err) + } + return hex.EncodeToString(bytes), nil +} diff --git a/main_dc/yalarba/api_yal/internal/domain/auth/router.go b/main_dc/yalarba/api_yal/internal/domain/auth/router.go index 92b4bdb..4bd6a0d 100644 --- a/main_dc/yalarba/api_yal/internal/domain/auth/router.go +++ b/main_dc/yalarba/api_yal/internal/domain/auth/router.go @@ -12,6 +12,8 @@ import ( // RegisterRoutes регистрирует маршруты аутентификации func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { + l := logger.Get() + l.Debug("Регистрация маршрутов для auth.go") // Создаем репозиторий и сервис accountRepo := repository.NewAccountRepository(db) @@ -25,7 +27,6 @@ func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) { authService := NewAuthService(accountRepo, authConfig) handler := NewAuthHandler(authService) - l := logger.Get() l.Debug("Регистрация маршрутов аутентификации") r.Route("/auth", func(r chi.Router) { diff --git a/main_dc/yalarba/api_yal/internal/domain/comment/dto.go b/main_dc/yalarba/api_yal/internal/domain/comment/dto.go new file mode 100644 index 0000000..0fc729d --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/comment/dto.go @@ -0,0 +1,25 @@ +package comment + +import ( + "time" +) + +// CommentShortResponse - краткий ответ для комментария +type CommentShortResponse struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + OwnerID uint `json:"owner_id"` + OwnerName string `json:"owner_name,omitempty"` + Text string `json:"text"` +} + +// CreateCommentRequest - DTO для создания комментария +type CreateCommentRequest struct { + FeedbackID uint `json:"feedback_id" binding:"required"` + Text string `json:"text" binding:"required"` +} + +// UpdateCommentRequest - DTO для обновления комментария +type UpdateCommentRequest struct { + Text *string `json:"text" binding:"required"` +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/feetback/dto.go b/main_dc/yalarba/api_yal/internal/domain/feetback/dto.go new file mode 100644 index 0000000..7fdee9a --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/feetback/dto.go @@ -0,0 +1,6 @@ +package feetback + +import ( + +) + diff --git a/main_dc/yalarba/api_yal/internal/domain/object/dto.go b/main_dc/yalarba/api_yal/internal/domain/object/dto.go new file mode 100644 index 0000000..8dbff86 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/object/dto.go @@ -0,0 +1,179 @@ +package object + +import ( + "api_yal/internal/domain/account" + "api_yal/internal/domain/comment" + "time" + "api_yal/internal/models" +) + +// ==================== Object DTOs ==================== + +// CreateObjectRequest - DTO для создания объекта +type CreateObjectRequest struct { + OwnerID uint `json:"owner_id" binding:"required"` + ShortName string `json:"short_name" binding:"required,min=1,max=255"` + LongName string `json:"long_name"` + Type string `json:"type"` + Phone string `json:"phone"` + Email string `json:"email" binding:"omitempty,email"` + Site string `json:"site" binding:"omitempty,url"` + ShortDescription string `json:"short_description"` + Description string `json:"description"` + Address string `json:"address"` + Latitude float64 `json:"latitude" binding:"omitempty,latitude"` + Longitude float64 `json:"longitude" binding:"omitempty,longitude"` + IsActive *bool `json:"is_active"` // указатель, чтобы отличать false от отсутствия значения + IsVerified *bool `json:"is_verified"` +} + +// UpdateObjectRequest - DTO для обновления объекта (все поля опциональны) +type UpdateObjectRequest struct { + ShortName *string `json:"short_name" binding:"omitempty,min=1,max=255"` + LongName *string `json:"long_name"` + Type *string `json:"type"` + Phone *string `json:"phone"` + Email *string `json:"email" binding:"omitempty,email"` + Site *string `json:"site" binding:"omitempty,url"` + ShortDescription *string `json:"short_description"` + Description *string `json:"description"` + Address *string `json:"address"` + Latitude *float64 `json:"latitude" binding:"omitempty,latitude"` + Longitude *float64 `json:"longitude" binding:"omitempty,longitude"` + IsActive *bool `json:"is_active"` + IsVerified *bool `json:"is_verified"` +} + +// ObjectResponse - DTO для полного ответа с объектом (включая связанные данные) +type ObjectResponse struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `json:"deleted_at,omitempty"` + OwnerID uint `json:"owner_id"` + Owner *account.AccountResponse `json:"owner,omitempty"` + ShortName string `json:"short_name"` + LongName string `json:"long_name"` + Type string `json:"type"` + Phone string `json:"phone"` + Email string `json:"email"` + Site string `json:"site"` + ShortDescription string `json:"short_description"` + Description string `json:"description"` + Address string `json:"address"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + IsActive bool `json:"is_active"` + IsVerified bool `json:"is_verified"` + FeedbackCount int `json:"feedback_count"` + TouristRating *RatingResponse `json:"tourist_rating,omitempty"` + EntrepreneurRating *RatingResponse `json:"entrepreneur_rating,omitempty"` + Feedbacks []FeedbackShortResponse `json:"feedbacks,omitempty"` +} + +// ObjectShortResponse - DTO для краткого ответа (списки, вложенные данные) +type ObjectShortResponse struct { + ID uint `json:"id"` + ShortName string `json:"short_name"` + LongName string `json:"long_name"` + Type string `json:"type"` + Address string `json:"address"` + IsActive bool `json:"is_active"` + IsVerified bool `json:"is_verified"` + FeedbackCount int `json:"feedback_count"` + // Агрегированные рейтинги для списка + TouristAverageScore float64 `json:"tourist_average_score,omitempty"` + EntrepreneurAverageScore float64 `json:"entrepreneur_average_score,omitempty"` +} + +// ObjectListResponse - DTO для списка объектов с пагинацией +type ObjectListResponse struct { + Items []ObjectShortResponse `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} + +// ==================== Feedback DTOs ==================== + +// CreateFeedbackRequest - DTO для создания отзыва +type CreateFeedbackRequest struct { + ObjectID uint `json:"object_id" binding:"required"` + Platform models.PlatformType `json:"platform" binding:"required,oneof=entrepreneur tourist"` + Score int `json:"score" binding:"required,min=1,max=5"` + Text string `json:"text" binding:"required"` +} + +// UpdateFeedbackRequest - DTO для обновления отзыва +type UpdateFeedbackRequest struct { + Score *int `json:"score" binding:"omitempty,min=1,max=5"` + Text *string `json:"text"` +} + +// FeedbackResponse - DTO для полного ответа с отзывом +type FeedbackResponse struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `json:"deleted_at,omitempty"` + OwnerID uint `json:"owner_id"` + Owner *account.AccountResponse `json:"owner,omitempty"` + ObjectID uint `json:"object_id"` + Object *ObjectShortResponse `json:"object,omitempty"` + Platform models.PlatformType `json:"platform"` + Score int `json:"score"` + Text string `json:"text"` + CommentCount int `json:"comment_count"` + Comments []comment.CommentShortResponse `json:"comments,omitempty"` +} + +// FeedbackShortResponse - DTO для краткого ответа (вложенный в объект) +type FeedbackShortResponse struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + OwnerID uint `json:"owner_id"` + OwnerName string `json:"owner_name,omitempty"` // из Account + Platform models.PlatformType `json:"platform"` + Score int `json:"score"` + Text string `json:"text"` +} + +// ==================== Rating DTOs ==================== + +// RatingResponse - DTO для ответа с рейтингом +type RatingResponse struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Platform models.PlatformType `json:"platform"` + AverageScore float64 `json:"average_score"` + TotalVotes int `json:"total_votes"` + VoteBreakdown VoteBreakdownDTO `json:"vote_breakdown"` +} + +// VoteBreakdownDTO - DTO для детализации оценок +type VoteBreakdownDTO struct { + Score1 int `json:"score_1"` + Score2 int `json:"score_2"` + Score3 int `json:"score_3"` + Score4 int `json:"score_4"` + Score5 int `json:"score_5"` +} + +// CreateRatingVoteRequest - DTO для голосования +type CreateRatingVoteRequest struct { + Platform models.PlatformType `json:"platform" binding:"required,oneof=entrepreneur tourist"` + TargetID uint `json:"target_id" binding:"required"` // ObjectID + Score int `json:"score" binding:"required,min=1,max=5"` +} + +// RatingVoteResponse - DTO для ответа о голосе пользователя +type RatingVoteResponse struct { + ID uint `json:"id"` + CreatedAt time.Time `json:"created_at"` + Platform models.PlatformType `json:"platform"` + TargetID uint `json:"target_id"` + VoterID uint `json:"voter_id"` + Score int `json:"score"` +} diff --git a/main_dc/yalarba/api_yal/internal/domain/object/errors.go b/main_dc/yalarba/api_yal/internal/domain/object/errors.go new file mode 100644 index 0000000..4fed9e9 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/object/errors.go @@ -0,0 +1,11 @@ +package object + +import "errors" + +var ( + ErrObjectNotFound = errors.New("object not found") + ErrInvalidOwnerID = errors.New("invalid owner ID") + ErrShortNameRequired = errors.New("short name is required") + ErrAlreadyVoted = errors.New("user has already voted") + ErrNotImplemented = errors.New("feature not implemented yet") +) \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/object/handler.go b/main_dc/yalarba/api_yal/internal/domain/object/handler.go new file mode 100644 index 0000000..8479711 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/object/handler.go @@ -0,0 +1,296 @@ +package object + +import ( + "encoding/json" + "net/http" + "strconv" + + "api_yal/internal/middleware" + "github.com/go-chi/chi/v5" +) + +type ObjectHandler struct { + objectService ObjectService +} + +func NewObjectHandler(objectService ObjectService) *ObjectHandler { + return &ObjectHandler{ + objectService: objectService, + } +} + +// GetObjectByID обрабатывает GET /objects/{id} +func (h *ObjectHandler) GetObjectByID(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) + if err != nil { + http.Error(w, "Invalid object ID", http.StatusBadRequest) + return + } + + response, err := h.objectService.GetObjectByID(r.Context(), uint(id)) + if err != nil { + h.handleError(w, err) + return + } + + h.respondWithJSON(w, http.StatusOK, response) +} + +// CreateObject обрабатывает POST /objects +func (h *ObjectHandler) CreateObject(w http.ResponseWriter, r *http.Request) { + var req CreateObjectRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Получаем ID пользователя из контекста (из AuthMiddleware) + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + // Устанавливаем owner_id из аутентифицированного пользователя + req.OwnerID = userID + + response, err := h.objectService.CreateObject(r.Context(), &req) + if err != nil { + h.handleError(w, err) + return + } + + h.respondWithJSON(w, http.StatusCreated, response) +} + +// UpdateObject обрабатывает PUT /objects/{id} +func (h *ObjectHandler) UpdateObject(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) + if err != nil { + http.Error(w, "Invalid object ID", http.StatusBadRequest) + return + } + + var req UpdateObjectRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + response, err := h.objectService.UpdateObject(r.Context(), uint(id), &req) + if err != nil { + h.handleError(w, err) + return + } + + h.respondWithJSON(w, http.StatusOK, response) +} + +// DeleteObject обрабатывает DELETE /objects/{id} +func (h *ObjectHandler) DeleteObject(w http.ResponseWriter, r *http.Request) { + id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) + if err != nil { + http.Error(w, "Invalid object ID", http.StatusBadRequest) + return + } + + if err := h.objectService.DeleteObject(r.Context(), uint(id)); err != nil { + h.handleError(w, err) + return + } + + w.WriteHeader(http.StatusNoContent) +} + +// ListObjects обрабатывает GET /objects +func (h *ObjectHandler) ListObjects(w http.ResponseWriter, r *http.Request) { + req := &ListObjectsRequest{ + Page: h.getQueryParamInt(r, "page", 1), + PageSize: h.getQueryParamInt(r, "page_size", 10), + Type: r.URL.Query().Get("type"), + Query: r.URL.Query().Get("q"), + } + + if statusStr := r.URL.Query().Get("is_active"); statusStr != "" { + isActive, err := strconv.ParseBool(statusStr) + if err == nil { + req.Status = &isActive + } + } + + response, err := h.objectService.ListObjects(r.Context(), req) + if err != nil { + h.handleError(w, err) + return + } + + h.respondWithJSON(w, http.StatusOK, response) +} + +// GetObjectsByOwner обрабатывает GET /objects/owner/{ownerId} +func (h *ObjectHandler) GetObjectsByOwner(w http.ResponseWriter, r *http.Request) { + ownerID, err := strconv.ParseUint(chi.URLParam(r, "ownerId"), 10, 32) + if err != nil { + http.Error(w, "Invalid owner ID", http.StatusBadRequest) + return + } + + page := h.getQueryParamInt(r, "page", 1) + pageSize := h.getQueryParamInt(r, "page_size", 10) + + response, err := h.objectService.GetObjectsByOwner(r.Context(), uint(ownerID), page, pageSize) + if err != nil { + h.handleError(w, err) + return + } + + h.respondWithJSON(w, http.StatusOK, response) +} + +// SearchObjects обрабатывает GET /objects/search +func (h *ObjectHandler) SearchObjects(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query().Get("q") + if query == "" { + http.Error(w, "Search query is required", http.StatusBadRequest) + return + } + + page := h.getQueryParamInt(r, "page", 1) + pageSize := h.getQueryParamInt(r, "page_size", 10) + + response, err := h.objectService.SearchObjects(r.Context(), query, page, pageSize) + if err != nil { + h.handleError(w, err) + return + } + + h.respondWithJSON(w, http.StatusOK, response) +} + +// GetNearbyObjects обрабатывает GET /objects/nearby +func (h *ObjectHandler) GetNearbyObjects(w http.ResponseWriter, r *http.Request) { + lat, err := strconv.ParseFloat(r.URL.Query().Get("lat"), 64) + if err != nil { + http.Error(w, "Invalid latitude", http.StatusBadRequest) + return + } + + lng, err := strconv.ParseFloat(r.URL.Query().Get("lng"), 64) + if err != nil { + http.Error(w, "Invalid longitude", http.StatusBadRequest) + return + } + + radius, err := strconv.ParseFloat(r.URL.Query().Get("radius"), 64) + if err != nil || radius <= 0 { + radius = 1000 // По умолчанию 1 км + } + + page := h.getQueryParamInt(r, "page", 1) + pageSize := h.getQueryParamInt(r, "page_size", 10) + + response, err := h.objectService.GetNearbyObjects(r.Context(), lat, lng, radius, page, pageSize) + if err != nil { + h.handleError(w, err) + return + } + + h.respondWithJSON(w, http.StatusOK, response) +} + +// CreateFeedback обрабатывает POST /objects/{id}/feedbacks +func (h *ObjectHandler) CreateFeedback(w http.ResponseWriter, r *http.Request) { + objectID, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) + if err != nil { + http.Error(w, "Invalid object ID", http.StatusBadRequest) + return + } + + var req CreateFeedbackRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + req.ObjectID = uint(objectID) + + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + response, err := h.objectService.CreateFeedback(r.Context(), &req, userID) + if err != nil { + h.handleError(w, err) + return + } + + h.respondWithJSON(w, http.StatusCreated, response) +} + +// CreateRatingVote обрабатывает POST /objects/{id}/ratings +func (h *ObjectHandler) CreateRatingVote(w http.ResponseWriter, r *http.Request) { + objectID, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32) + if err != nil { + http.Error(w, "Invalid object ID", http.StatusBadRequest) + return + } + + var req CreateRatingVoteRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + req.TargetID = uint(objectID) + + userID, ok := r.Context().Value(middleware.UserIDKey).(uint) + if !ok { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + response, err := h.objectService.CreateRatingVote(r.Context(), &req, userID) + if err != nil { + h.handleError(w, err) + return + } + + h.respondWithJSON(w, http.StatusCreated, response) +} + +// Вспомогательные методы + +func (h *ObjectHandler) respondWithJSON(w http.ResponseWriter, status int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(data); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + +func (h *ObjectHandler) handleError(w http.ResponseWriter, err error) { + switch err { + case ErrObjectNotFound: + http.Error(w, err.Error(), http.StatusNotFound) + case ErrInvalidOwnerID, ErrShortNameRequired, ErrAlreadyVoted: + http.Error(w, err.Error(), http.StatusBadRequest) + case ErrNotImplemented: + http.Error(w, err.Error(), http.StatusNotImplemented) + default: + http.Error(w, "Internal server error", http.StatusInternalServerError) + } +} + +func (h *ObjectHandler) getQueryParamInt(r *http.Request, key string, defaultValue int) int { + value := r.URL.Query().Get(key) + if value == "" { + return defaultValue + } + intValue, err := strconv.Atoi(value) + if err != nil { + return defaultValue + } + return intValue +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/object/router.go b/main_dc/yalarba/api_yal/internal/domain/object/router.go new file mode 100644 index 0000000..80c3c8c --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/object/router.go @@ -0,0 +1,47 @@ +package object + +import ( + "api_yal/internal/logger" + "api_yal/internal/middleware" + "api_yal/internal/repository" + + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +// RegisterObjects регистрирует маршруты для работы с объектами +func RegisterObjects(r chi.Router, db *gorm.DB, jwtSecret string) { + l := logger.Get() + l.Debug("Регистрация маршрутов объектов") + + objectRepo := repository.NewObjectRepository(db) + objectService := NewObjectService(objectRepo) + objectHandler := NewObjectHandler(objectService) + + // Публичные маршруты (не требуют аутентификации) + r.Group(func(r chi.Router) { + r.Get("/objects", objectHandler.ListObjects) + r.Get("/objects/search", objectHandler.SearchObjects) + r.Get("/objects/nearby", objectHandler.GetNearbyObjects) + r.Get("/objects/{id}", objectHandler.GetObjectByID) + r.Get("/objects/owner/{ownerId}", objectHandler.GetObjectsByOwner) + }) + + // Защищенные маршруты (требуют аутентификации) + r.Group(func(r chi.Router) { + r.Use(middleware.AuthMiddleware(jwtSecret)) + + // CRUD для объектов + r.Post("/objects", objectHandler.CreateObject) + r.Put("/objects/{id}", objectHandler.UpdateObject) + r.Delete("/objects/{id}", objectHandler.DeleteObject) + + // Отзывы + r.Post("/objects/{id}/feedbacks", objectHandler.CreateFeedback) + + // Рейтинги + r.Post("/objects/{id}/ratings", objectHandler.CreateRatingVote) + }) + + l.Debug("Маршруты объектов зарегистрированы") +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/object/service.go b/main_dc/yalarba/api_yal/internal/domain/object/service.go new file mode 100644 index 0000000..0489e05 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/object/service.go @@ -0,0 +1,731 @@ +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 + } + + object := &models.Object{ + OwnerID: req.OwnerID, + ShortName: req.ShortName, + LongName: req.LongName, + Type: req.Type, + 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, + 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.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.ShortName != nil { + object.ShortName = *req.ShortName + } + if req.LongName != nil { + object.LongName = *req.LongName + } + if req.Type != nil { + object.Type = *req.Type + } + 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.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, + ShortName: object.ShortName, + LongName: object.LongName, + Type: object.Type, + 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, + 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, + } + } + } + + return resp +} + +func (s *objectServiceImpl) mapToObjectShortResponse(object *models.Object) ObjectShortResponse { + return ObjectShortResponse{ + ID: object.ID, + ShortName: object.ShortName, + LongName: object.LongName, + Type: object.Type, + Address: object.Address, + IsActive: object.IsActive, + IsVerified: object.IsVerified, + 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 +} diff --git a/main_dc/yalarba/api_yal/internal/domain/object/types.go b/main_dc/yalarba/api_yal/internal/domain/object/types.go new file mode 100644 index 0000000..4a09261 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/object/types.go @@ -0,0 +1,19 @@ +package object + +// ListObjectsRequest параметры для получения списка объектов +type ListObjectsRequest struct { + Page int + PageSize int + Type string + Status *bool + Query string +} + +// FeedbackListResponse ответ со списком отзывов +type FeedbackListResponse struct { + Items []FeedbackResponse `json:"items"` + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` + TotalPages int `json:"total_pages"` +} \ No newline at end of file diff --git a/main_dc/yalarba/api_yal/internal/domain/rating/dto.go b/main_dc/yalarba/api_yal/internal/domain/rating/dto.go new file mode 100644 index 0000000..7253ef6 --- /dev/null +++ b/main_dc/yalarba/api_yal/internal/domain/rating/dto.go @@ -0,0 +1,6 @@ +package rating + +import ( + +) + diff --git a/main_dc/yalarba/api_yal/internal/models/rating.go b/main_dc/yalarba/api_yal/internal/models/rating.go index 56d98fa..371d639 100644 --- a/main_dc/yalarba/api_yal/internal/models/rating.go +++ b/main_dc/yalarba/api_yal/internal/models/rating.go @@ -39,7 +39,7 @@ type Rating struct { // Используется для отображения распределения голосов от 1 до 5 type VoteBreakdown struct { // Base содержит общие поля для всех моделей - Base Base `gorm:"embedded"` + Base `gorm:"embedded"` // RatingID - идентификатор рейтинга, к которому относится детализация RatingID uint `json:"rating_id"` @@ -61,7 +61,7 @@ type VoteBreakdown struct { type RatingVote struct { // Base содержит общие поля для всех моделей: // ID, CreatedAt, UpdatedAt, DeletedAt (история обновлений) - Base Base `gorm:"embedded"` + Base `gorm:"embedded"` // Platform - платформа, на которой был сделан голос Platform PlatformType `json:"platform"`