// handlers/personal_best_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 PersonalBestHandler struct { logger logger.LoggerInterface personalBestService service.PersonalBestService } func NewPersonalBestHandler(personalBestService service.PersonalBestService) *PersonalBestHandler { return &PersonalBestHandler{ logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "personal_best"))), personalBestService: personalBestService, } } // CreatePersonalBest создает новый личный рекорд func (h *PersonalBestHandler) CreatePersonalBest(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling create 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("create personal best failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } var req models.PersonalBestCreateRequest 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 req.DistanceType == "" { h.logger.Warn("create personal best failed - distance type required") utils.RespondWithError(w, http.StatusBadRequest, "Distance type is required") return } if req.Time == "" { h.logger.Warn("create personal best failed - time required") utils.RespondWithError(w, http.StatusBadRequest, "Time is required") return } if req.Date.IsZero() { h.logger.Warn("create personal best failed - date required") utils.RespondWithError(w, http.StatusBadRequest, "Date is required") return } personalBest, err := h.personalBestService.CreatePersonalBest(user.ID, req) if err != nil { h.logger.Error("failed to create personal best", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create personal best: "+err.Error()) return } h.logger.Info("personal best created successfully", zap.Uint("user_id", user.ID), zap.Uint("personal_best_id", personalBest.ID), zap.String("distance_type", string(personalBest.DistanceType)), ) utils.RespondWithJSON(w, http.StatusCreated, personalBest) } // GetPersonalBest возвращает личный рекорд по ID func (h *PersonalBestHandler) GetPersonalBest(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling get personal best request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) idStr := chi.URLParam(r, "id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { h.logger.Warn("invalid personal best ID", zap.String("id", idStr)) utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID") return } personalBest, err := h.personalBestService.GetPersonalBestByID(uint(id)) if err != nil { h.logger.Error("failed to get personal best", zap.Error(err)) if err.Error() == "record not found" { utils.RespondWithError(w, http.StatusNotFound, "Personal best not found") } else { utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal best: "+err.Error()) } return } h.logger.Info("personal best retrieved successfully", zap.Uint("personal_best_id", personalBest.ID), ) utils.RespondWithJSON(w, http.StatusOK, personalBest) } // GetUserPersonalBests возвращает все личные рекорды пользователя func (h *PersonalBestHandler) GetUserPersonalBests(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling get user personal bests 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 personal bests failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } personalBests, err := h.personalBestService.GetUserPersonalBests(user.ID) if err != nil { h.logger.Error("failed to get personal bests", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal bests: "+err.Error()) return } h.logger.Info("user personal bests retrieved successfully", zap.Uint("user_id", user.ID), zap.Int("count", len(personalBests)), ) utils.RespondWithJSON(w, http.StatusOK, personalBests) } // UpdatePersonalBest обновляет личный рекорд func (h *PersonalBestHandler) 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 } idStr := chi.URLParam(r, "id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { h.logger.Warn("invalid personal best ID", zap.String("id", idStr)) utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID") return } var req models.PersonalBestUpdateRequest 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 } personalBest, err := h.personalBestService.UpdatePersonalBest(uint(id), user.ID, req) if err != nil { h.logger.Error("failed to update personal best", zap.Error(err)) if err.Error() == "record not found" { utils.RespondWithError(w, http.StatusNotFound, "Personal best not found or access denied") } else { utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update personal best: "+err.Error()) } return } h.logger.Info("personal best updated successfully", zap.Uint("personal_best_id", personalBest.ID), zap.Uint("user_id", user.ID), ) utils.RespondWithJSON(w, http.StatusOK, personalBest) } // DeletePersonalBest удаляет личный рекорд func (h *PersonalBestHandler) DeletePersonalBest(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling delete 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("delete personal best failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { h.logger.Warn("invalid personal best ID", zap.String("id", idStr)) utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID") return } err = h.personalBestService.DeletePersonalBest(uint(id), user.ID) if err != nil { h.logger.Error("failed to delete personal best", zap.Error(err)) if err.Error() == "record not found" { utils.RespondWithError(w, http.StatusNotFound, "Personal best not found or access denied") } else { utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete personal best: "+err.Error()) } return } h.logger.Info("personal best deleted successfully", zap.Uint("personal_best_id", uint(id)), zap.Uint("user_id", user.ID), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Personal best deleted successfully", }) } // GetPersonalBestsByDistance возвращает личные рекорды по дистанции func (h *PersonalBestHandler) GetPersonalBestsByDistance(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling get personal bests by 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("get personal bests by distance failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } distanceType := models.DistanceType(chi.URLParam(r, "distanceType")) if distanceType == "" { h.logger.Warn("distance type parameter is required") utils.RespondWithError(w, http.StatusBadRequest, "Distance type parameter is required") return } // Валидация типа дистанции validDistances := map[models.DistanceType]bool{ models.Distance5K: true, models.Distance10K: true, models.DistanceHalf: true, models.DistanceFull: true, models.DistanceOther: true, } if !validDistances[distanceType] { h.logger.Warn("invalid distance type", zap.String("distance_type", string(distanceType))) utils.RespondWithError(w, http.StatusBadRequest, "Invalid distance type") return } personalBests, err := h.personalBestService.GetPersonalBestsByDistance(user.ID, distanceType) if err != nil { h.logger.Error("failed to get personal bests by distance", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal bests: "+err.Error()) return } h.logger.Info("personal bests by distance retrieved successfully", zap.Uint("user_id", user.ID), zap.String("distance_type", string(distanceType)), zap.Int("count", len(personalBests)), ) utils.RespondWithJSON(w, http.StatusOK, personalBests) } // GetBestByDistance возвращает лучший результат на дистанции func (h *PersonalBestHandler) GetBestByDistance(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling get best by 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("get best by distance failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } distanceType := models.DistanceType(chi.URLParam(r, "distanceType")) if distanceType == "" { h.logger.Warn("distance type parameter is required") utils.RespondWithError(w, http.StatusBadRequest, "Distance type parameter is required") return } best, err := h.personalBestService.GetBestByDistance(user.ID, distanceType) if err != nil { if err.Error() == "record not found" { h.logger.Info("no personal best found for distance", zap.Uint("user_id", user.ID), zap.String("distance_type", string(distanceType)), ) utils.RespondWithJSON(w, http.StatusOK, nil) return } h.logger.Error("failed to get best by distance", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get best result: "+err.Error()) return } h.logger.Info("best by distance retrieved successfully", zap.Uint("user_id", user.ID), zap.String("distance_type", string(distanceType)), ) utils.RespondWithJSON(w, http.StatusOK, best) } // GetPersonalBestsSummary возвращает сводку лучших результатов func (h *PersonalBestHandler) GetPersonalBestsSummary(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling get personal bests summary 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 personal bests summary failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } summary, err := h.personalBestService.GetPersonalBestsSummary(user.ID) if err != nil { h.logger.Error("failed to get personal bests summary", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal bests summary: "+err.Error()) return } h.logger.Info("personal bests summary retrieved successfully", zap.Uint("user_id", user.ID), ) utils.RespondWithJSON(w, http.StatusOK, summary) } // VerifyPersonalBest подтверждает личный рекорд func (h *PersonalBestHandler) VerifyPersonalBest(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling verify 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("verify personal best failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } idStr := chi.URLParam(r, "id") id, err := strconv.ParseUint(idStr, 10, 32) if err != nil { h.logger.Warn("invalid personal best ID", zap.String("id", idStr)) utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID") return } err = h.personalBestService.VerifyPersonalBest(uint(id), user.ID) if err != nil { h.logger.Error("failed to verify personal best", zap.Error(err)) if err.Error() == "record not found" { utils.RespondWithError(w, http.StatusNotFound, "Personal best not found or access denied") } else { utils.RespondWithError(w, http.StatusInternalServerError, "Failed to verify personal best: "+err.Error()) } return } h.logger.Info("personal best verified successfully", zap.Uint("personal_best_id", uint(id)), zap.Uint("user_id", user.ID), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "message": "Personal best verified successfully", }) } // GetRecentPersonalBests возвращает последние личные рекорды func (h *PersonalBestHandler) GetRecentPersonalBests(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling get recent personal bests 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 recent personal bests failed - authentication required") utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required") return } limit := 10 // default limit limitStr := r.URL.Query().Get("limit") if limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { limit = l } } personalBests, err := h.personalBestService.GetRecentPersonalBests(user.ID, limit) if err != nil { h.logger.Error("failed to get recent personal bests", zap.Error(err)) utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get recent personal bests: "+err.Error()) return } h.logger.Info("recent personal bests retrieved successfully", zap.Uint("user_id", user.ID), zap.Int("limit", limit), zap.Int("count", len(personalBests)), ) utils.RespondWithJSON(w, http.StatusOK, personalBests) } // CalculatePace вычисляет темп func (h *PersonalBestHandler) CalculatePace(w http.ResponseWriter, r *http.Request) { h.logger.Info("handling calculate pace request", zap.String("method", r.Method), zap.String("path", r.URL.Path), zap.String("remote_addr", r.RemoteAddr), ) var req struct { Time string `json:"time"` DistanceType models.DistanceType `json:"distance_type"` } 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 req.Time == "" || req.DistanceType == "" { h.logger.Warn("time and distance type are required") utils.RespondWithError(w, http.StatusBadRequest, "Time and distance type are required") return } pace, err := h.personalBestService.CalculatePace(req.Time, req.DistanceType) if err != nil { h.logger.Error("failed to calculate pace", zap.Error(err)) utils.RespondWithError(w, http.StatusBadRequest, "Failed to calculate pace: "+err.Error()) return } h.logger.Info("pace calculated successfully", zap.String("time", req.Time), zap.String("distance_type", string(req.DistanceType)), zap.String("pace", pace), ) utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{ "time": req.Time, "distance_type": req.DistanceType, "pace": pace, }) }