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:
@@ -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 {
|
||||||
userStatsRepo repository.UserStatsRepository
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserStatsService(userStatsRepo repository.UserStatsRepository) *UserStatsService {
|
type userStatsService struct {
|
||||||
return &UserStatsService{
|
logger logger.LoggerInterface
|
||||||
|
userStatsRepo repository.UserStatsRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserStatsService(userStatsRepo repository.UserStatsRepository) 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
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user