modified: serv_nginx/api_bb/internal/handlers/handlers.go
new file: serv_nginx/api_bb/internal/handlers/user_workout_handler.go modified: serv_nginx/api_bb/internal/models/workout.go modified: serv_nginx/api_bb/internal/repository/workout_repository.go modified: serv_nginx/api_bb/internal/routes/routes.go new file: serv_nginx/api_bb/internal/service/user_workout_service.go new file: serv_nginx/api_bb/pkg/utils/response.go new file: serv_nginx/api_bb/pkg/utils/validation.go add workout EndPoints for workout struct, CRUD operations and logics
This commit is contained in:
@@ -18,6 +18,7 @@ type Handler struct {
|
||||
newsHandler *NewsHandler
|
||||
reviewHandler *ReviewHandler
|
||||
userStatsHandler *UserStatsHandler
|
||||
userWorkoutHandler *UserWorkoutHandler
|
||||
// Здесь будут добавлены другие обработчики
|
||||
// userHandler *UserHandler
|
||||
// eventHandler *EventHandler
|
||||
@@ -31,6 +32,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
|
||||
commentRepo := repository.NewCommentRepository(db)
|
||||
reviewRepo := repository.NewReviewRepository(db)
|
||||
userStatsRepo := repository.NewUserStatsRepository(db)
|
||||
userWorkoutRepo := repository.NewWorkoutRepository(db)
|
||||
|
||||
// Initialize logger
|
||||
baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер
|
||||
@@ -43,6 +45,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
|
||||
newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger)
|
||||
reviewService := service.NewReviewService(reviewRepo, baseLogger)
|
||||
userStatsService := service.NewUserStatsService(userStatsRepo)
|
||||
userWorkoutService := service.NewWorkoutService(userWorkoutRepo)
|
||||
|
||||
// Инициализация обработчиков
|
||||
healthHandler := NewHealthHandler()
|
||||
@@ -52,6 +55,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
|
||||
avatarHandler := NewAvatarHandler(avatarService)
|
||||
reviewHandler := NewReviewHandler(reviewService, baseLogger)
|
||||
userStatsHandler := NewUserStatsHandler(userStatsService)
|
||||
userWorkoutHandler := NewUserWorkoutHandler(userWorkoutService)
|
||||
|
||||
return &Handler{
|
||||
healthHandler: healthHandler,
|
||||
@@ -61,6 +65,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
|
||||
avatarHandler: avatarHandler,
|
||||
reviewHandler: reviewHandler,
|
||||
userStatsHandler: userStatsHandler,
|
||||
userWorkoutHandler: userWorkoutHandler,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,3 +97,7 @@ func (h *Handler) ReviewHandler() *ReviewHandler {
|
||||
func (h *Handler) UserStatsHandler() *UserStatsHandler {
|
||||
return h.userStatsHandler
|
||||
}
|
||||
|
||||
func (h *Handler) UserWorkoutHandler() *UserWorkoutHandler {
|
||||
return h.userWorkoutHandler
|
||||
}
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
// handlers/user_workout_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type UserWorkoutHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
workoutService service.WorkoutService
|
||||
}
|
||||
|
||||
func NewUserWorkoutHandler(workoutService service.WorkoutService) *UserWorkoutHandler {
|
||||
return &UserWorkoutHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user_workout"))),
|
||||
workoutService: workoutService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWorkout создает новую тренировку
|
||||
func (h *UserWorkoutHandler) CreateWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling create workout request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("create workout failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.WorkoutCreateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("create workout failed - validation error", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("creating new workout",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("type", string(req.Type)),
|
||||
zap.Float64("distance", req.Distance),
|
||||
zap.Int("duration", req.Duration),
|
||||
)
|
||||
|
||||
// Создаем тренировку
|
||||
workout, err := h.workoutService.CreateWorkout(user.ID, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to create workout in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create workout: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout created successfully",
|
||||
zap.Uint("workout_id", workout.ID),
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "Workout created successfully",
|
||||
"workout": workout,
|
||||
})
|
||||
}
|
||||
|
||||
// GetWorkouts возвращает список тренировок пользователя
|
||||
func (h *UserWorkoutHandler) GetWorkouts(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get workouts request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get workouts failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
workouts, err := h.workoutService.GetUserWorkouts(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get user workouts from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get workouts: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("user workouts retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("workouts_count", len(workouts)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, workouts)
|
||||
}
|
||||
|
||||
// GetWorkoutByID возвращает тренировку по ID
|
||||
func (h *UserWorkoutHandler) GetWorkoutByID(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get workout by ID request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get workout failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID тренировки из URL параметров
|
||||
workoutIDStr := chi.URLParam(r, "id")
|
||||
workoutID, err := strconv.ParseUint(workoutIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid workout ID", zap.String("workout_id", workoutIDStr))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout ID")
|
||||
return
|
||||
}
|
||||
|
||||
workout, err := h.workoutService.GetWorkoutByID(user.ID, uint(workoutID))
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get workout from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Workout not found: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, workout)
|
||||
}
|
||||
|
||||
// UpdateWorkout обновляет тренировку
|
||||
func (h *UserWorkoutHandler) UpdateWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling update workout request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update workout failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID тренировки из URL параметров
|
||||
workoutIDStr := chi.URLParam(r, "id")
|
||||
workoutID, err := strconv.ParseUint(workoutIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid workout ID", zap.String("workout_id", workoutIDStr))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.WorkoutUpdateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("update workout failed - validation error", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("updating workout",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
zap.String("type", string(req.Type)),
|
||||
)
|
||||
|
||||
// Обновляем тренировку
|
||||
workout, err := h.workoutService.UpdateWorkout(user.ID, uint(workoutID), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to update workout in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update workout: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout updated successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Workout updated successfully",
|
||||
"workout": workout,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteWorkout удаляет тренировку
|
||||
func (h *UserWorkoutHandler) DeleteWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling delete workout request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("delete workout failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID тренировки из URL параметров
|
||||
workoutIDStr := chi.URLParam(r, "id")
|
||||
workoutID, err := strconv.ParseUint(workoutIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid workout ID", zap.String("workout_id", workoutIDStr))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout ID")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("deleting workout",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
)
|
||||
|
||||
// Удаляем тренировку
|
||||
if err := h.workoutService.DeleteWorkout(user.ID, uint(workoutID)); err != nil {
|
||||
h.logger.Error("failed to delete workout in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete workout: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout deleted successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Workout deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetWorkoutStats возвращает статистику тренировок
|
||||
func (h *UserWorkoutHandler) GetWorkoutStats(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get workout stats request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get workout stats failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := h.workoutService.GetWorkoutStats(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get workout stats from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get workout stats: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout stats retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("total_workouts", stats.TotalWorkouts),
|
||||
zap.Float64("total_distance", stats.TotalDistance),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// GetWorkoutsByType возвращает тренировки по типу
|
||||
func (h *UserWorkoutHandler) GetWorkoutsByType(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get workouts by type request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get workouts by type failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем тип тренировки из URL параметров
|
||||
workoutType := models.WorkoutType(chi.URLParam(r, "type"))
|
||||
|
||||
// Валидация типа тренировки
|
||||
validTypes := map[models.WorkoutType]bool{
|
||||
models.WorkoutTypeEasy: true,
|
||||
models.WorkoutTypeTempo: true,
|
||||
models.WorkoutTypeInterval: true,
|
||||
models.WorkoutTypeLong: true,
|
||||
models.WorkoutTypeRecovery: true,
|
||||
}
|
||||
|
||||
if !validTypes[workoutType] {
|
||||
h.logger.Warn("invalid workout type", zap.String("type", string(workoutType)))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout type")
|
||||
return
|
||||
}
|
||||
|
||||
workouts, err := h.workoutService.GetWorkoutsByType(user.ID, workoutType)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get workouts by type from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("type", string(workoutType)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get workouts: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workouts by type retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("type", string(workoutType)),
|
||||
zap.Int("workouts_count", len(workouts)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, workouts)
|
||||
}
|
||||
@@ -56,20 +56,20 @@ type WorkoutCreateRequest struct {
|
||||
Type WorkoutType `json:"type" validate:"required,oneof=easy tempo interval long recovery"`
|
||||
Distance float64 `json:"distance_km" validate:"required,min=0.1,max=1000"`
|
||||
Duration int `json:"duration_min" validate:"required,min=1,max=1440"`
|
||||
Pace string `json:"pace" validate:"max=20"`
|
||||
Calories int `json:"calories" validate:"min=0,max=5000"`
|
||||
Notes string `json:"notes" validate:"max=1000"`
|
||||
Pace string `json:"pace" validate:"maxlen=20"`
|
||||
Calories int `json:"calories" validate:"minint=0,maxint=5000"`
|
||||
Notes string `json:"notes" validate:"maxlen=1000"`
|
||||
Date time.Time `json:"date" validate:"required"`
|
||||
}
|
||||
|
||||
// DTO для обновления тренировки
|
||||
type WorkoutUpdateRequest struct {
|
||||
Type WorkoutType `json:"type" validate:"omitempty,oneof=easy tempo interval long recovery"`
|
||||
Distance float64 `json:"distance_km" validate:"omitempty,min=0.1,max=1000"`
|
||||
Duration int `json:"duration_min" validate:"omitempty,min=1,max=1440"`
|
||||
Pace string `json:"pace" validate:"omitempty,max=20"`
|
||||
Calories int `json:"calories" validate:"omitempty,min=0,max=5000"`
|
||||
Notes string `json:"notes" validate:"omitempty,max=1000"`
|
||||
Type WorkoutType `json:"type" validate:"oneof=easy tempo interval long recovery"`
|
||||
Distance float64 `json:"distance_km" validate:"min=0.1,max=1000"`
|
||||
Duration int `json:"duration_min" validate:"min=1,max=1440"`
|
||||
Pace string `json:"pace" validate:"maxlen=20"`
|
||||
Calories int `json:"calories" validate:"minint=0,maxint=5000"`
|
||||
Notes string `json:"notes" validate:"maxlen=1000"`
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -26,6 +27,10 @@ type workoutRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("record not found")
|
||||
)
|
||||
|
||||
func NewWorkoutRepository(db *gorm.DB) WorkoutRepository {
|
||||
return &workoutRepository{db: db}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,20 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
r.Post("/weekly/reset", allHandler.UserStatsHandler().ResetWeeklyDistance)
|
||||
r.Post("/monthly/reset", allHandler.UserStatsHandler().ResetMonthlyDistance)
|
||||
})
|
||||
|
||||
// Маршруты для тренировок
|
||||
r.Route("/workouts", func(r chi.Router) {
|
||||
r.Post("/", allHandler.UserWorkoutHandler().CreateWorkout)
|
||||
r.Get("/", allHandler.UserWorkoutHandler().GetWorkouts)
|
||||
r.Get("/stats", allHandler.UserWorkoutHandler().GetWorkoutStats)
|
||||
r.Get("/type/{type}", allHandler.UserWorkoutHandler().GetWorkoutsByType)
|
||||
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
r.Get("/", allHandler.UserWorkoutHandler().GetWorkoutByID)
|
||||
r.Put("/", allHandler.UserWorkoutHandler().UpdateWorkout)
|
||||
r.Delete("/", allHandler.UserWorkoutHandler().DeleteWorkout)
|
||||
})
|
||||
})
|
||||
// Здесь будут другие защищенные маршруты пользователя
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
// service/user_workout_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type WorkoutService interface {
|
||||
CreateWorkout(userID uint, req *models.WorkoutCreateRequest) (*models.Workout, error)
|
||||
GetUserWorkouts(userID uint) ([]models.Workout, error)
|
||||
GetWorkoutByID(userID uint, workoutID uint) (*models.Workout, error)
|
||||
UpdateWorkout(userID uint, workoutID uint, req *models.WorkoutUpdateRequest) (*models.Workout, error)
|
||||
DeleteWorkout(userID uint, workoutID uint) error
|
||||
GetWorkoutStats(userID uint) (*models.WorkoutStatsResponse, error)
|
||||
GetWorkoutsByType(userID uint, workoutType models.WorkoutType) ([]models.Workout, error)
|
||||
GetLatestWorkouts(userID uint, limit int) ([]models.Workout, error)
|
||||
}
|
||||
|
||||
type workoutService struct {
|
||||
workoutRepo repository.WorkoutRepository
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewWorkoutService(workoutRepo repository.WorkoutRepository) WorkoutService {
|
||||
return &workoutService{
|
||||
workoutRepo: workoutRepo,
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("service", "workout"))),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWorkout создает новую тренировку
|
||||
func (s *workoutService) CreateWorkout(userID uint, req *models.WorkoutCreateRequest) (*models.Workout, error) {
|
||||
s.logger.Info("creating new workout",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("type", string(req.Type)),
|
||||
zap.Float64("distance", req.Distance),
|
||||
)
|
||||
|
||||
// Создаем модель тренировки
|
||||
workout := &models.Workout{
|
||||
UserID: userID,
|
||||
Type: req.Type,
|
||||
Distance: req.Distance,
|
||||
Duration: req.Duration,
|
||||
Pace: req.Pace,
|
||||
Calories: req.Calories,
|
||||
Notes: req.Notes,
|
||||
Date: req.Date,
|
||||
}
|
||||
|
||||
// Сохраняем в репозитории
|
||||
if err := s.workoutRepo.Create(workout); err != nil {
|
||||
s.logger.Error("failed to create workout in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("workout created successfully",
|
||||
zap.Uint("workout_id", workout.ID),
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
return workout, nil
|
||||
}
|
||||
|
||||
// GetUserWorkouts возвращает все тренировки пользователя
|
||||
func (s *workoutService) GetUserWorkouts(userID uint) ([]models.Workout, error) {
|
||||
s.logger.Debug("getting user workouts", zap.Uint("user_id", userID))
|
||||
|
||||
workouts, err := s.workoutRepo.FindByUserID(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get user workouts from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("retrieved user workouts",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("count", len(workouts)),
|
||||
)
|
||||
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
// GetWorkoutByID возвращает тренировку по ID
|
||||
func (s *workoutService) GetWorkoutByID(userID uint, workoutID uint) (*models.Workout, error) {
|
||||
s.logger.Debug("getting workout by ID",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
workout, err := s.workoutRepo.FindByID(workoutID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get workout from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, что тренировка принадлежит пользователю
|
||||
if workout.UserID != userID {
|
||||
s.logger.Warn("workout access denied - user mismatch",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_user_id", workout.UserID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
return nil, repository.ErrNotFound
|
||||
}
|
||||
|
||||
s.logger.Debug("workout retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
return workout, nil
|
||||
}
|
||||
|
||||
// UpdateWorkout обновляет тренировку
|
||||
func (s *workoutService) UpdateWorkout(userID uint, workoutID uint, req *models.WorkoutUpdateRequest) (*models.Workout, error) {
|
||||
s.logger.Info("updating workout",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
// Сначала получаем существующую тренировку
|
||||
workout, err := s.GetWorkoutByID(userID, workoutID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Обновляем только переданные поля
|
||||
if req.Type != "" {
|
||||
workout.Type = req.Type
|
||||
}
|
||||
if req.Distance > 0 {
|
||||
workout.Distance = req.Distance
|
||||
}
|
||||
if req.Duration > 0 {
|
||||
workout.Duration = req.Duration
|
||||
}
|
||||
if req.Pace != "" {
|
||||
workout.Pace = req.Pace
|
||||
}
|
||||
if req.Calories > 0 {
|
||||
workout.Calories = req.Calories
|
||||
}
|
||||
if req.Notes != "" {
|
||||
workout.Notes = req.Notes
|
||||
}
|
||||
if !req.Date.IsZero() {
|
||||
workout.Date = req.Date
|
||||
}
|
||||
|
||||
// Сохраняем обновления
|
||||
if err := s.workoutRepo.Update(workout); err != nil {
|
||||
s.logger.Error("failed to update workout in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("workout updated successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
return workout, nil
|
||||
}
|
||||
|
||||
// DeleteWorkout удаляет тренировку
|
||||
func (s *workoutService) DeleteWorkout(userID uint, workoutID uint) error {
|
||||
s.logger.Info("deleting workout",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
// Проверяем, что тренировка существует и принадлежит пользователю
|
||||
workout, err := s.GetWorkoutByID(userID, workoutID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Удаляем тренировку
|
||||
if err := s.workoutRepo.Delete(workout.ID); err != nil {
|
||||
s.logger.Error("failed to delete workout from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("workout deleted successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWorkoutStats возвращает статистику тренировок
|
||||
func (s *workoutService) GetWorkoutStats(userID uint) (*models.WorkoutStatsResponse, error) {
|
||||
s.logger.Debug("getting workout stats", zap.Uint("user_id", userID))
|
||||
|
||||
stats, err := s.workoutRepo.GetWorkoutStats(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get workout stats from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("workout stats retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("total_workouts", stats.TotalWorkouts),
|
||||
zap.Float64("total_distance", stats.TotalDistance),
|
||||
)
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetWorkoutsByType возвращает тренировки по типу
|
||||
func (s *workoutService) GetWorkoutsByType(userID uint, workoutType models.WorkoutType) ([]models.Workout, error) {
|
||||
s.logger.Debug("getting workouts by type",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("type", string(workoutType)),
|
||||
)
|
||||
|
||||
workouts, err := s.workoutRepo.GetByType(userID, workoutType)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get workouts by type from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("type", string(workoutType)),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("workouts by type retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("type", string(workoutType)),
|
||||
zap.Int("count", len(workouts)),
|
||||
)
|
||||
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
// GetLatestWorkouts возвращает последние тренировки
|
||||
func (s *workoutService) GetLatestWorkouts(userID uint, limit int) ([]models.Workout, error) {
|
||||
s.logger.Debug("getting latest workouts",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("limit", limit),
|
||||
)
|
||||
|
||||
workouts, err := s.workoutRepo.GetLatestWorkouts(userID, limit)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get latest workouts from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("limit", limit),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("latest workouts retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("limit", limit),
|
||||
zap.Int("count", len(workouts)),
|
||||
)
|
||||
|
||||
return workouts, nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// pkg/utils/response.go (дополнение)
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RespondWithValidationError отправляет ответ с ошибками валидации
|
||||
func RespondWithValidationError(w http.ResponseWriter, validationError error) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"error": "Validation failed",
|
||||
"details": GetValidationErrors(validationError),
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
// pkg/utils/validation.go
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ValidationError представляет ошибку валидации
|
||||
type ValidationError struct {
|
||||
Field string `json:"field"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// ValidationResult содержит результат валидации
|
||||
type ValidationResult struct {
|
||||
IsValid bool
|
||||
Errors []ValidationError
|
||||
}
|
||||
|
||||
// TagOptions содержит опции из тега validate
|
||||
type TagOptions struct {
|
||||
Required bool
|
||||
Min *float64
|
||||
Max *float64
|
||||
MinInt *int64
|
||||
MaxInt *int64
|
||||
OneOf []string
|
||||
Email bool
|
||||
MaxLength *int
|
||||
MinLength *int
|
||||
Custom string
|
||||
}
|
||||
|
||||
// ValidateStruct валидирует структуру на основе тегов validate
|
||||
func ValidateStruct(s interface{}) error {
|
||||
val := reflect.ValueOf(s)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("ValidateStruct expects a struct, got %T", s)
|
||||
}
|
||||
|
||||
var errors []ValidationError
|
||||
typ := val.Type()
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
|
||||
// Пропускаем неэкспортируемые поля
|
||||
if !field.CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
tag := fieldType.Tag.Get("validate")
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
options := parseTagOptions(tag)
|
||||
fieldName := getFieldName(fieldType)
|
||||
|
||||
// Валидация поля
|
||||
if err := validateField(field, fieldName, options); err != nil {
|
||||
errors = append(errors, err...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return &ValidationResult{
|
||||
IsValid: false,
|
||||
Errors: errors,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseTagOptions парсит тег validate и возвращает опции
|
||||
func parseTagOptions(tag string) TagOptions {
|
||||
options := TagOptions{}
|
||||
parts := strings.Split(tag, ",")
|
||||
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
|
||||
switch {
|
||||
case part == "required":
|
||||
options.Required = true
|
||||
case part == "email":
|
||||
options.Email = true
|
||||
case strings.HasPrefix(part, "min="):
|
||||
if val, err := strconv.ParseFloat(part[4:], 64); err == nil {
|
||||
options.Min = &val
|
||||
}
|
||||
case strings.HasPrefix(part, "max="):
|
||||
if val, err := strconv.ParseFloat(part[4:], 64); err == nil {
|
||||
options.Max = &val
|
||||
}
|
||||
case strings.HasPrefix(part, "minint="):
|
||||
if val, err := strconv.ParseInt(part[7:], 10, 64); err == nil {
|
||||
options.MinInt = &val
|
||||
}
|
||||
case strings.HasPrefix(part, "maxint="):
|
||||
if val, err := strconv.ParseInt(part[7:], 10, 64); err == nil {
|
||||
options.MaxInt = &val
|
||||
}
|
||||
case strings.HasPrefix(part, "oneof="):
|
||||
options.OneOf = strings.Split(part[6:], " ")
|
||||
case strings.HasPrefix(part, "maxlen="):
|
||||
if val, err := strconv.Atoi(part[7:]); err == nil {
|
||||
options.MaxLength = &val
|
||||
}
|
||||
case strings.HasPrefix(part, "minlen="):
|
||||
if val, err := strconv.Atoi(part[7:]); err == nil {
|
||||
options.MinLength = &val
|
||||
}
|
||||
case strings.HasPrefix(part, "custom="):
|
||||
options.Custom = part[7:]
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
// getFieldName возвращает имя поля для сообщений об ошибках
|
||||
func getFieldName(field reflect.StructField) string {
|
||||
jsonTag := field.Tag.Get("json")
|
||||
if jsonTag != "" {
|
||||
parts := strings.Split(jsonTag, ",")
|
||||
if parts[0] != "" {
|
||||
return parts[0]
|
||||
}
|
||||
}
|
||||
return field.Name
|
||||
}
|
||||
|
||||
// validateField валидирует отдельное поле
|
||||
func validateField(field reflect.Value, fieldName string, options TagOptions) []ValidationError {
|
||||
var errors []ValidationError
|
||||
|
||||
// Проверка required
|
||||
if options.Required {
|
||||
if isEmptyValue(field) {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: "field is required",
|
||||
})
|
||||
return errors // Если поле обязательно и пустое, дальше не проверяем
|
||||
}
|
||||
}
|
||||
|
||||
// Если поле пустое и не обязательное, дальше не проверяем
|
||||
if isEmptyValue(field) {
|
||||
return errors
|
||||
}
|
||||
|
||||
// Валидация в зависимости от типа поля
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
errors = append(errors, validateString(field.String(), fieldName, options)...)
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
errors = append(errors, validateInt(field.Int(), fieldName, options)...)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
errors = append(errors, validateFloat(field.Float(), fieldName, options)...)
|
||||
case reflect.Struct:
|
||||
// Для time.Time и других структур
|
||||
if field.Type().String() == "time.Time" {
|
||||
errors = append(errors, validateTime(field.Interface().(time.Time), fieldName, options)...)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// validateString валидирует строковые поля
|
||||
func validateString(value, fieldName string, options TagOptions) []ValidationError {
|
||||
var errors []ValidationError
|
||||
|
||||
// Проверка email
|
||||
if options.Email {
|
||||
if !isValidEmail(value) {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: "invalid email format",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Проверка длины строки
|
||||
if options.MinLength != nil && len(value) < *options.MinLength {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("minimum length is %d characters", *options.MinLength),
|
||||
})
|
||||
}
|
||||
|
||||
if options.MaxLength != nil && len(value) > *options.MaxLength {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("maximum length is %d characters", *options.MaxLength),
|
||||
})
|
||||
}
|
||||
|
||||
// Проверка oneof
|
||||
if len(options.OneOf) > 0 {
|
||||
valid := false
|
||||
for _, allowed := range options.OneOf {
|
||||
if value == allowed {
|
||||
valid = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("must be one of: %s", strings.Join(options.OneOf, ", ")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// validateInt валидирует целочисленные поля
|
||||
func validateInt(value int64, fieldName string, options TagOptions) []ValidationError {
|
||||
var errors []ValidationError
|
||||
|
||||
if options.MinInt != nil && value < *options.MinInt {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("minimum value is %d", *options.MinInt),
|
||||
})
|
||||
}
|
||||
|
||||
if options.MaxInt != nil && value > *options.MaxInt {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("maximum value is %d", *options.MaxInt),
|
||||
})
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// validateFloat валидирует поля с плавающей точкой
|
||||
func validateFloat(value float64, fieldName string, options TagOptions) []ValidationError {
|
||||
var errors []ValidationError
|
||||
|
||||
if options.Min != nil && value < *options.Min {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("minimum value is %.2f", *options.Min),
|
||||
})
|
||||
}
|
||||
|
||||
if options.Max != nil && value > *options.Max {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: fmt.Sprintf("maximum value is %.2f", *options.Max),
|
||||
})
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// validateTime валидирует временные поля
|
||||
func validateTime(value time.Time, fieldName string, options TagOptions) []ValidationError {
|
||||
var errors []ValidationError
|
||||
|
||||
// Проверка, что дата не нулевая
|
||||
if value.IsZero() && options.Required {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: "date is required",
|
||||
})
|
||||
}
|
||||
|
||||
// Проверка, что дата не в будущем (пример кастомной валидации)
|
||||
if options.Custom == "not_future" && value.After(time.Now()) {
|
||||
errors = append(errors, ValidationError{
|
||||
Field: fieldName,
|
||||
Message: "date cannot be in the future",
|
||||
})
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// isEmptyValue проверяет, является ли значение пустым
|
||||
func isEmptyValue(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
return v.String() == ""
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Bool:
|
||||
return !v.Bool()
|
||||
case reflect.Struct:
|
||||
if v.Type().String() == "time.Time" {
|
||||
return v.Interface().(time.Time).IsZero()
|
||||
}
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
return v.IsNil()
|
||||
case reflect.Slice, reflect.Map, reflect.Array:
|
||||
return v.Len() == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isValidEmail проверяет валидность email
|
||||
func isValidEmail(email string) bool {
|
||||
emailRegex := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
|
||||
matched, _ := regexp.MatchString(emailRegex, email)
|
||||
return matched
|
||||
}
|
||||
|
||||
// Error возвращает строковое представление ошибок валидации
|
||||
func (vr *ValidationResult) Error() string {
|
||||
var errorMessages []string
|
||||
for _, err := range vr.Errors {
|
||||
errorMessages = append(errorMessages, err.Error())
|
||||
}
|
||||
return strings.Join(errorMessages, "; ")
|
||||
}
|
||||
|
||||
// GetValidationErrors возвращает ошибки валидации в структурированном виде
|
||||
func GetValidationErrors(err error) []ValidationError {
|
||||
if vr, ok := err.(*ValidationResult); ok {
|
||||
return vr.Errors
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogValidationErrors логирует ошибки валидации
|
||||
func LogValidationErrors(logger *zap.Logger, err error, context string) {
|
||||
if vr, ok := err.(*ValidationResult); ok {
|
||||
for _, validationErr := range vr.Errors {
|
||||
logger.Warn("validation error",
|
||||
zap.String("context", context),
|
||||
zap.String("field", validationErr.Field),
|
||||
zap.String("error", validationErr.Message),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user