348 lines
11 KiB
Go
348 lines
11 KiB
Go
// 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",
|
|
})
|
|
} |