modified: serv_nginx/api_bb/internal/handlers/handlers.go

new file:   serv_nginx/api_bb/internal/handlers/user_stats_handler.go
	modified:   serv_nginx/api_bb/internal/routes/routes.go
	modified:   serv_nginx/api_bb/internal/service/user_stats_service.go
add EndPoints for user stats
This commit is contained in:
2025-10-19 05:56:09 +05:00
parent f7db0a07eb
commit c55310e2e0
4 changed files with 595 additions and 16 deletions
@@ -17,6 +17,7 @@ type Handler struct {
avatarHandler *AvatarHandler avatarHandler *AvatarHandler
newsHandler *NewsHandler newsHandler *NewsHandler
reviewHandler *ReviewHandler reviewHandler *ReviewHandler
userStatsHandler *UserStatsHandler
// Здесь будут добавлены другие обработчики // Здесь будут добавлены другие обработчики
// userHandler *UserHandler // userHandler *UserHandler
// eventHandler *EventHandler // eventHandler *EventHandler
@@ -29,6 +30,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
newsRepo := repository.NewNewsRepository(db) newsRepo := repository.NewNewsRepository(db)
commentRepo := repository.NewCommentRepository(db) commentRepo := repository.NewCommentRepository(db)
reviewRepo := repository.NewReviewRepository(db) reviewRepo := repository.NewReviewRepository(db)
userStatsRepo := repository.NewUserStatsRepository(db)
// Initialize logger // Initialize logger
baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер
@@ -40,6 +42,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
avatarService := service.NewAvatarService(userRepo, baseLogger) avatarService := service.NewAvatarService(userRepo, baseLogger)
newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger) newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger)
reviewService := service.NewReviewService(reviewRepo, baseLogger) reviewService := service.NewReviewService(reviewRepo, baseLogger)
userStatsService := service.NewUserStatsService(userStatsRepo)
// Инициализация обработчиков // Инициализация обработчиков
healthHandler := NewHealthHandler() healthHandler := NewHealthHandler()
@@ -48,6 +51,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
newsHandler := NewNewsHandler(newsService, baseLogger) newsHandler := NewNewsHandler(newsService, baseLogger)
avatarHandler := NewAvatarHandler(avatarService) avatarHandler := NewAvatarHandler(avatarService)
reviewHandler := NewReviewHandler(reviewService, baseLogger) reviewHandler := NewReviewHandler(reviewService, baseLogger)
userStatsHandler := NewUserStatsHandler(userStatsService)
return &Handler{ return &Handler{
healthHandler: healthHandler, healthHandler: healthHandler,
@@ -56,6 +60,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
newsHandler: newsHandler, newsHandler: newsHandler,
avatarHandler: avatarHandler, avatarHandler: avatarHandler,
reviewHandler: reviewHandler, reviewHandler: reviewHandler,
userStatsHandler: userStatsHandler,
} }
} }
@@ -68,7 +73,7 @@ func (h *Handler) AuthHandler() *AuthHandler {
return h.authHandler return h.authHandler
} }
func (h *Handler) UserHandler() *UserHandler { // ДОБАВЛЕН геттер для UserHandler func (h *Handler) UserHandler() *UserHandler {
return h.userHandler return h.userHandler
} }
@@ -83,3 +88,7 @@ func (h *Handler) NewsHandler() *NewsHandler {
func (h *Handler) ReviewHandler() *ReviewHandler { func (h *Handler) ReviewHandler() *ReviewHandler {
return h.reviewHandler return h.reviewHandler
} }
func (h *Handler) UserStatsHandler() *UserStatsHandler {
return h.userStatsHandler
}
@@ -0,0 +1,348 @@
// 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",
})
}
@@ -74,6 +74,15 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
r.Delete("/delete", allHandler.AvatarHandler().DeleteAvatar) r.Delete("/delete", allHandler.AvatarHandler().DeleteAvatar)
r.Get("/{filename}", allHandler.AvatarHandler().GetAvatar) r.Get("/{filename}", allHandler.AvatarHandler().GetAvatar)
}) })
r.Route("/stats", func(r chi.Router) {
r.Get("/", allHandler.UserStatsHandler().GetUserStats)
r.Get("/{userID}", allHandler.UserStatsHandler().GetUserStatsByID)
r.Post("/workout", allHandler.UserStatsHandler().IncrementWorkout)
r.Put("/personal-best", allHandler.UserStatsHandler().UpdatePersonalBest)
r.Post("/weekly/reset", allHandler.UserStatsHandler().ResetWeeklyDistance)
r.Post("/monthly/reset", allHandler.UserStatsHandler().ResetMonthlyDistance)
})
// Здесь будут другие защищенные маршруты пользователя // Здесь будут другие защищенные маршруты пользователя
}) })
@@ -1,45 +1,258 @@
// services/user_stats_service.go // service/user_stats_service.go
package service package service
import ( import (
"time" "time"
"api_bb/internal/models" "api_bb/internal/models"
"api_bb/internal/repository" "api_bb/internal/repository"
"api_bb/pkg/logger"
"go.uber.org/zap"
) )
type UserStatsService struct { type UserStatsService interface {
GetUserStats(userID uint) (*models.UserStatsResponse, error)
UpdatePersonalBest(userID uint, distanceType string, time string) error
IncrementWorkout(userID uint, distance float64, duration int) error
ResetWeeklyDistance(userID uint) error
ResetMonthlyDistance(userID uint) error
CreateUserStats(userID uint) error
}
type userStatsService struct {
logger logger.LoggerInterface
userStatsRepo repository.UserStatsRepository userStatsRepo repository.UserStatsRepository
} }
func NewUserStatsService(userStatsRepo repository.UserStatsRepository) *UserStatsService { func NewUserStatsService(userStatsRepo repository.UserStatsRepository) UserStatsService {
return &UserStatsService{ return &userStatsService{
logger: logger.NewWrapper(logger.Get().With(zap.String("service", "user_stats"))),
userStatsRepo: userStatsRepo, userStatsRepo: userStatsRepo,
} }
} }
func (s *UserStatsService) AddWorkout(userID uint, distance float64, duration int, workoutTime time.Time) error { // GetUserStats возвращает статистику пользователя в формате DTO
// Обновляем общую статистику func (s *userStatsService) GetUserStats(userID uint) (*models.UserStatsResponse, error) {
if err := s.userStatsRepo.IncrementWorkouts(userID, distance, duration); err != nil { s.logger.Info("getting user stats",
zap.Uint("user_id", userID),
)
stats, err := s.userStatsRepo.GetUserStatsResponse(userID)
if err != nil {
s.logger.Error("failed to get user stats from repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return nil, err
}
s.logger.Debug("user stats retrieved successfully",
zap.Uint("user_id", userID),
zap.Float64("total_distance", stats.TotalDistance),
zap.Int("workouts_count", stats.WorkoutsCount),
)
return stats, nil
}
// UpdatePersonalBest обновляет личный рекорд пользователя
func (s *userStatsService) UpdatePersonalBest(userID uint, distanceType string, time string) error {
s.logger.Info("updating personal best",
zap.Uint("user_id", userID),
zap.String("distance_type", distanceType),
zap.String("time", time),
)
// Проверяем существование статистики пользователя
_, err := s.userStatsRepo.GetByUserID(userID)
if err != nil {
s.logger.Warn("user stats not found, creating new stats",
zap.Uint("user_id", userID),
)
if err := s.CreateUserStats(userID); err != nil {
return err
}
}
if err := s.userStatsRepo.UpdatePersonalBest(userID, distanceType, time); err != nil {
s.logger.Error("failed to update personal best in repository",
zap.Uint("user_id", userID),
zap.String("distance_type", distanceType),
zap.Error(err),
)
return err return err
} }
// Обновляем серии s.logger.Info("personal best updated successfully",
if err := s.userStatsRepo.UpdateStreaks(userID, workoutTime); err != nil { zap.Uint("user_id", userID),
zap.String("distance_type", distanceType),
zap.String("time", time),
)
return nil
}
// IncrementWorkout увеличивает счетчик тренировок и обновляет статистику
func (s *userStatsService) IncrementWorkout(userID uint, distance float64, duration int) error {
s.logger.Info("incrementing workout stats",
zap.Uint("user_id", userID),
zap.Float64("distance", distance),
zap.Int("duration", duration),
)
// Проверяем существование статистики пользователя
_, err := s.userStatsRepo.GetByUserID(userID)
if err != nil {
s.logger.Warn("user stats not found, creating new stats",
zap.Uint("user_id", userID),
)
if err := s.CreateUserStats(userID); err != nil {
return err
}
}
// Обновляем серии тренировок
currentTime := time.Now()
if err := s.userStatsRepo.UpdateStreaks(userID, currentTime); err != nil {
s.logger.Error("failed to update streaks in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err return err
} }
// Обновляем недельный и месячный пробег // Обновляем недельный и месячный пробег
if err := s.userStatsRepo.UpdateWeeklyDistance(userID, distance); err != nil { if err := s.userStatsRepo.UpdateWeeklyDistance(userID, distance); err != nil {
s.logger.Error("failed to update weekly distance in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err return err
} }
return s.userStatsRepo.UpdateMonthlyDistance(userID, distance) if err := s.userStatsRepo.UpdateMonthlyDistance(userID, distance); err != nil {
s.logger.Error("failed to update monthly distance in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
// Увеличиваем счетчик тренировок и обновляем общие показатели
if err := s.userStatsRepo.IncrementWorkouts(userID, distance, duration); err != nil {
s.logger.Error("failed to increment workouts in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
s.logger.Info("workout stats incremented successfully",
zap.Uint("user_id", userID),
zap.Float64("distance", distance),
zap.Int("duration", duration),
)
return nil
} }
func (s *UserStatsService) GetUserStats(userID uint) (*models.UserStatsResponse, error) { // ResetWeeklyDistance сбрасывает недельный пробег
return s.userStatsRepo.GetUserStatsResponse(userID) func (s *userStatsService) ResetWeeklyDistance(userID uint) error {
s.logger.Info("resetting weekly distance",
zap.Uint("user_id", userID),
)
userStats, err := s.userStatsRepo.GetByUserID(userID)
if err != nil {
s.logger.Error("failed to get user stats for weekly reset",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
userStats.WeeklyDistance = 0
if err := s.userStatsRepo.Update(userStats); err != nil {
s.logger.Error("failed to reset weekly distance in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
s.logger.Info("weekly distance reset successfully",
zap.Uint("user_id", userID),
)
return nil
} }
func (s *UserStatsService) UpdatePersonalBest(userID uint, distanceType string, time string) error { // ResetMonthlyDistance сбрасывает месячный пробег
return s.userStatsRepo.UpdatePersonalBest(userID, distanceType, time) func (s *userStatsService) ResetMonthlyDistance(userID uint) error {
s.logger.Info("resetting monthly distance",
zap.Uint("user_id", userID),
)
userStats, err := s.userStatsRepo.GetByUserID(userID)
if err != nil {
s.logger.Error("failed to get user stats for monthly reset",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
userStats.MonthlyDistance = 0
if err := s.userStatsRepo.Update(userStats); err != nil {
s.logger.Error("failed to reset monthly distance in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
s.logger.Info("monthly distance reset successfully",
zap.Uint("user_id", userID),
)
return nil
}
// CreateUserStats создает начальную статистику для пользователя
func (s *userStatsService) CreateUserStats(userID uint) error {
s.logger.Info("creating user stats",
zap.Uint("user_id", userID),
)
userStats := &models.UserStats{
UserID: userID,
TotalDistance: 0,
TotalTime: 0,
AvgPace: "0:00",
WorkoutsCount: 0,
CurrentStreak: 0,
LongestStreak: 0,
WeeklyDistance: 0,
MonthlyDistance: 0,
Best5K: "",
Best10K: "",
BestHalf: "",
BestMarathon: "",
LastWorkout: time.Time{},
}
if err := s.userStatsRepo.Create(userStats); err != nil {
s.logger.Error("failed to create user stats in repository",
zap.Uint("user_id", userID),
zap.Error(err),
)
return err
}
s.logger.Info("user stats created successfully",
zap.Uint("user_id", userID),
)
return nil
} }