// handlers/user_stats_handler.go package handlers import ( "net/http" "strconv" "api_bb/internal/service" "api_bb/pkg/logger" "api_bb/pkg/middleware" "api_bb/pkg/utils" "github.com/go-chi/chi/v5" "go.uber.org/zap" ) type UserStatsHandler struct { logger logger.LoggerInterface userStatsService service.UserStatsService } func NewUserStatsHandler(userStatsService service.UserStatsService) *UserStatsHandler { return &UserStatsHandler{ logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user_stats"))), userStatsService: userStatsService, } } // GetUserStats возвращает статистику текущего пользователя func (h *UserStatsHandler) GetUserStats(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling get user 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 user stats failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } // Получаем статистику через сервис stats, err := h.userStatsService.GetUserStats(user.ID) if err != nil { h.logger.Error("failed to get user stats from service", zap.Uint("user_id", user.ID), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user stats: "+err.Error()) return } h.logger.Info("user stats retrieved successfully", zap.Uint("user_id", user.ID), zap.Float64("total_distance", stats.TotalDistance), zap.Int("workouts_count", stats.WorkoutsCount), ) utils.RespondWithJSON(w, http.StatusOK, stats) } // GetUserStatsByID возвращает статистику пользователя по ID (для администраторов) func (h *UserStatsHandler) GetUserStatsByID(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling get user stats by ID request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) // Получаем текущего пользователя для проверки прав currentUser, ok := middleware.GetUserFromContext(r.Context()) if !ok { h.logger.Warn("get user stats by ID failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } // Проверяем права администратора if currentUser.Role != "admin" { h.logger.Warn("get user stats by ID failed - insufficient permissions", zap.Uint("user_id", currentUser.ID), zap.String("role", currentUser.Role), ) utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions") return } // Получаем ID пользователя из параметров URL userIDStr := chi.URLParam(r, "userID") userID, err := strconv.ParseUint(userIDStr, 10, 32) if err != nil { h.logger.Warn("invalid user ID parameter", zap.String("user_id_param", userIDStr), zap.Error(err), ) utils.RespondWithError(w, http.StatusBadRequest, "Invalid user ID") return } // Получаем статистику через сервис stats, err := h.userStatsService.GetUserStats(uint(userID)) if err != nil { h.logger.Error("failed to get user stats by ID from service", zap.Uint("target_user_id", uint(userID)), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user stats: "+err.Error()) return } h.logger.Info("user stats by ID retrieved successfully", zap.Uint("admin_user_id", currentUser.ID), zap.Uint("target_user_id", uint(userID)), ) utils.RespondWithJSON(w, http.StatusOK, stats) } // UpdatePersonalBest обновляет личный рекорд пользователя func (h *UserStatsHandler) UpdatePersonalBest(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling update personal best 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 personal best failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } var req struct { DistanceType string `json:"distance_type"` Time string `json:"time"` } if err := utils.DecodeJSONBody(w, r, &req); err != nil { h.logger.Error("failed to decode update personal best request", zap.Error(err), ) utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error()) return } // Валидация обязательных полей if req.DistanceType == "" || req.Time == "" { h.logger.Warn("update personal best failed - missing required fields") utils.RespondWithError(w, http.StatusBadRequest, "Distance type and time are required") return } // Валидация типа дистанции validDistanceTypes := map[string]bool{ "5k": true, "10k": true, "half": true, "marathon": true, } if !validDistanceTypes[req.DistanceType] { h.logger.Warn("update personal best failed - invalid distance type", zap.String("distance_type", req.DistanceType), ) utils.RespondWithError(w, http.StatusBadRequest, "Invalid distance type. Must be: 5k, 10k, half, marathon") return } h.logger.Info("updating personal best", zap.Uint("user_id", user.ID), zap.String("distance_type", req.DistanceType), zap.String("time", req.Time), ) // Обновляем личный рекорд через сервис if err := h.userStatsService.UpdatePersonalBest(user.ID, req.DistanceType, req.Time); err != nil { h.logger.Error("failed to update personal best in service", zap.Uint("user_id", user.ID), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update personal best: "+err.Error()) return } h.logger.Info("personal best updated successfully", zap.Uint("user_id", user.ID), zap.String("distance_type", req.DistanceType), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Personal best updated successfully", "distance_type": req.DistanceType, "time": req.Time, }) } // IncrementWorkout увеличивает счетчик тренировок и обновляет статистику func (h *UserStatsHandler) IncrementWorkout(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling increment 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("increment workout failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } var req struct { Distance float64 `json:"distance"` Duration int `json:"duration"` } if err := utils.DecodeJSONBody(w, r, &req); err != nil { h.logger.Error("failed to decode increment workout request", zap.Error(err), ) utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error()) return } // Валидация данных тренировки if req.Distance <= 0 { h.logger.Warn("increment workout failed - invalid distance", zap.Float64("distance", req.Distance), ) utils.RespondWithError(w, http.StatusBadRequest, "Distance must be greater than 0") return } if req.Duration <= 0 { h.logger.Warn("increment workout failed - invalid duration", zap.Int("duration", req.Duration), ) utils.RespondWithError(w, http.StatusBadRequest, "Duration must be greater than 0") return } h.logger.Info("incrementing workout stats", zap.Uint("user_id", user.ID), zap.Float64("distance", req.Distance), zap.Int("duration", req.Duration), ) // Обновляем статистику через сервис if err := h.userStatsService.IncrementWorkout(user.ID, req.Distance, req.Duration); err != nil { h.logger.Error("failed to increment workout in service", zap.Uint("user_id", user.ID), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update workout stats: "+err.Error()) return } h.logger.Info("workout stats incremented successfully", zap.Uint("user_id", user.ID), zap.Float64("distance", req.Distance), zap.Int("duration", req.Duration), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Workout stats updated successfully", "distance": req.Distance, "duration": req.Duration, }) } // ResetWeeklyDistance сбрасывает недельный пробег func (h *UserStatsHandler) ResetWeeklyDistance(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling reset weekly distance 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("reset weekly distance failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } h.logger.Info("resetting weekly distance", zap.Uint("user_id", user.ID), ) // Сбрасываем недельный пробег через сервис if err := h.userStatsService.ResetWeeklyDistance(user.ID); err != nil { h.logger.Error("failed to reset weekly distance in service", zap.Uint("user_id", user.ID), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to reset weekly distance: "+err.Error()) return } h.logger.Info("weekly distance reset successfully", zap.Uint("user_id", user.ID), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Weekly distance reset successfully", }) } // ResetMonthlyDistance сбрасывает месячный пробег func (h *UserStatsHandler) ResetMonthlyDistance(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling reset monthly distance 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("reset monthly distance failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } h.logger.Info("resetting monthly distance", zap.Uint("user_id", user.ID), ) // Сбрасываем месячный пробег через сервис if err := h.userStatsService.ResetMonthlyDistance(user.ID); err != nil { h.logger.Error("failed to reset monthly distance in service", zap.Uint("user_id", user.ID), zap.Error(err), ) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to reset monthly distance: "+err.Error()) return } h.logger.Info("monthly distance reset successfully", zap.Uint("user_id", user.ID), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Monthly distance reset successfully", }) }