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
This commit is contained in:
2026-03-31 16:53:24 +05:00
parent 15eed69a45
commit 979c265e36
14 changed files with 1392 additions and 142 deletions
@@ -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)
@@ -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))
@@ -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())
}
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
}
@@ -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) {
@@ -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"`
}
@@ -0,0 +1,6 @@
package feetback
import (
)
@@ -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"`
}
@@ -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")
)
@@ -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
}
@@ -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("Маршруты объектов зарегистрированы")
}
@@ -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
}
@@ -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"`
}
@@ -0,0 +1,6 @@
package rating
import (
)
@@ -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"`