diff --git a/serv_nginx/api_bb/internal/database/migrate.go b/serv_nginx/api_bb/internal/database/migrate.go index 5c1a3e9..5d06dbd 100644 --- a/serv_nginx/api_bb/internal/database/migrate.go +++ b/serv_nginx/api_bb/internal/database/migrate.go @@ -24,15 +24,16 @@ func (d *Database) Migrate() error { &models.Achievement{}, &models.Event{}, &models.EventRegistration{}, + &models.PersonalBest{}, // Добавьте другие модели здесь } for _, model := range models { modelName := getModelName(model) zapLogger.Debug("migrating model", zap.String("model", modelName)) - + if err := d.DB.AutoMigrate(model); err != nil { - zapLogger.Error("failed to migrate model", + zapLogger.Error("failed to migrate model", zap.String("model", modelName), zap.Error(err), ) @@ -48,16 +49,16 @@ func (d *Database) Migrate() error { func (d *Database) MigrateModels(models ...interface{}) error { zapLogger := logger.Get() - zapLogger.Info("starting migration for specific models", + zapLogger.Info("starting migration for specific models", zap.Int("model_count", len(models)), ) for _, model := range models { modelName := getModelName(model) zapLogger.Debug("migrating model", zap.String("model", modelName)) - + if err := d.DB.AutoMigrate(model); err != nil { - zapLogger.Error("failed to migrate model", + zapLogger.Error("failed to migrate model", zap.String("model", modelName), zap.Error(err), ) @@ -90,7 +91,9 @@ func getModelName(model interface{}) string { return "Событие" case *models.EventRegistration: return "Администрирование события" + case *models.PersonalBest: + return "Персональные достижения" default: return "Unknown" } -} \ No newline at end of file +} diff --git a/serv_nginx/api_bb/internal/handlers/handlers.go b/serv_nginx/api_bb/internal/handlers/handlers.go index 37540e1..a900f2d 100644 --- a/serv_nginx/api_bb/internal/handlers/handlers.go +++ b/serv_nginx/api_bb/internal/handlers/handlers.go @@ -22,6 +22,7 @@ type Handler struct { userAchievementHandler *UserAchievementHandler eventHandler *EventHandler eventRegistrationHandler *EventRegistrationHandler + personalBestHandler *PersonalBestHandler // Здесь будут добавлены другие обработчики // userHandler *UserHandler // eventHandler *EventHandler @@ -39,6 +40,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { userAchievemenRepo := repository.NewAchievementRepository(db) eventRepo := repository.NewEventRepository(db) eventRegistrationRepo := repository.NewEventRegistrationRepository(db) + personalBestRepo := repository.NewPersonalBestRepository(db) // Initialize logger baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер @@ -55,6 +57,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { achievementService := service.NewAchievementService(userAchievemenRepo) eventRegistrationService := service.NewEventRegistrationService(eventRegistrationRepo, eventRepo, baseLogger) eventService := service.NewEventService(eventRepo, eventRegistrationRepo, baseLogger) + personalBestService := service.NewPersonalBestService(personalBestRepo, userStatsService) // Инициализация обработчиков healthHandler := NewHealthHandler() @@ -68,6 +71,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { userAchievementHandler := NewUserAchievementHandler(*achievementService) eventHandler := NewEventHandler(eventService) eventRegistrationHandler := NewEventRegistrationHandler(eventRegistrationService) + personalBestHandler := NewPersonalBestHandler(*personalBestService) return &Handler{ healthHandler: healthHandler, @@ -81,10 +85,15 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { userAchievementHandler: userAchievementHandler, eventHandler: eventHandler, eventRegistrationHandler: eventRegistrationHandler, + personalBestHandler: personalBestHandler, } } // Геттеры для обработчиков (опционально, для удобства) +func (h *Handler) PersonalBestHandler() *PersonalBestHandler { + return h.personalBestHandler +} + func (h *Handler) EventHandler() *EventHandler { return h.eventHandler } diff --git a/serv_nginx/api_bb/internal/handlers/personal_best_handler.go b/serv_nginx/api_bb/internal/handlers/personal_best_handler.go new file mode 100644 index 0000000..1594509 --- /dev/null +++ b/serv_nginx/api_bb/internal/handlers/personal_best_handler.go @@ -0,0 +1,506 @@ +// 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, + }) +} \ No newline at end of file diff --git a/serv_nginx/api_bb/internal/models/personal_best.go b/serv_nginx/api_bb/internal/models/personal_best.go index 0952c84..7815313 100644 --- a/serv_nginx/api_bb/internal/models/personal_best.go +++ b/serv_nginx/api_bb/internal/models/personal_best.go @@ -70,4 +70,16 @@ type PersonalBestUpdateRequest struct { EventName string `json:"event_name" validate:"omitempty,max=255"` Location string `json:"location" validate:"omitempty,max=255"` Verified bool `json:"verified"` +} + +// PersonalBestsSummary представляет сводку лучших результатов по дистанциям +type PersonalBestsSummary struct { + Best5K string `json:"best_5k,omitempty"` + Best5KPace string `json:"best_5k_pace,omitempty"` + Best10K string `json:"best_10k,omitempty"` + Best10KPace string `json:"best_10k_pace,omitempty"` + BestHalf string `json:"best_half_marathon,omitempty"` + BestHalfPace string `json:"best_half_marathon_pace,omitempty"` + BestMarathon string `json:"best_marathon,omitempty"` + BestMarathonPace string `json:"best_marathon_pace,omitempty"` } \ No newline at end of file diff --git a/serv_nginx/api_bb/internal/models/user_stats.go b/serv_nginx/api_bb/internal/models/user_stats.go index 81c727d..2371027 100644 --- a/serv_nginx/api_bb/internal/models/user_stats.go +++ b/serv_nginx/api_bb/internal/models/user_stats.go @@ -58,11 +58,4 @@ type UserStatsResponse struct { WeeklyDistance float64 `json:"weekly_distance"` MonthlyDistance float64 `json:"monthly_distance"` PersonalBests PersonalBestsSummary `json:"personal_bests"` -} - -type PersonalBestsSummary struct { - Best5K string `json:"best_5k"` - Best10K string `json:"best_10k"` - BestHalf string `json:"best_half"` - BestMarathon string `json:"best_marathon"` } \ No newline at end of file diff --git a/serv_nginx/api_bb/internal/repository/personal_best_repository.go b/serv_nginx/api_bb/internal/repository/personal_best_repository.go index e19edd2..5c4bacc 100644 --- a/serv_nginx/api_bb/internal/repository/personal_best_repository.go +++ b/serv_nginx/api_bb/internal/repository/personal_best_repository.go @@ -4,9 +4,9 @@ package repository import ( "time" - "gorm.io/gorm" "api_bb/internal/models" "api_bb/pkg/utils" + "gorm.io/gorm" ) type PersonalBestRepository interface { @@ -22,6 +22,8 @@ type PersonalBestRepository interface { GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) ExistsBetterTime(userID uint, distanceType models.DistanceType, time string) (bool, error) CalculatePace(timeStr string, distanceType models.DistanceType) (string, error) + GetRecentPersonalBests(userID uint, limit int) ([]models.PersonalBest, error) + GetByEventName(userID uint, eventName string) ([]models.PersonalBest, error) } type personalBestRepository struct { @@ -51,6 +53,7 @@ func (r *personalBestRepository) GetByID(id uint) (*models.PersonalBest, error) func (r *personalBestRepository) GetByUserID(userID uint) ([]models.PersonalBest, error) { var personalBests []models.PersonalBest err := r.db.Where("user_id = ?", userID). + Preload("User"). Order("distance_type, time"). Find(&personalBests).Error if err != nil { @@ -63,6 +66,7 @@ func (r *personalBestRepository) GetByUserID(userID uint) ([]models.PersonalBest func (r *personalBestRepository) GetByUserAndDistance(userID uint, distanceType models.DistanceType) ([]models.PersonalBest, error) { var personalBests []models.PersonalBest err := r.db.Where("user_id = ? AND distance_type = ?", userID, distanceType). + Preload("User"). Order("time"). Find(&personalBests).Error if err != nil { @@ -75,6 +79,7 @@ func (r *personalBestRepository) GetByUserAndDistance(userID uint, distanceType func (r *personalBestRepository) GetBestByDistance(userID uint, distanceType models.DistanceType) (*models.PersonalBest, error) { var personalBest models.PersonalBest err := r.db.Where("user_id = ? AND distance_type = ?", userID, distanceType). + Preload("User"). Order("time"). First(&personalBest).Error if err != nil { @@ -97,6 +102,7 @@ func (r *personalBestRepository) Delete(id uint) error { func (r *personalBestRepository) GetVerifiedByUserID(userID uint) ([]models.PersonalBest, error) { var personalBests []models.PersonalBest err := r.db.Where("user_id = ? AND verified = ?", userID, true). + Preload("User"). Order("distance_type, time"). Find(&personalBests).Error if err != nil { @@ -109,6 +115,7 @@ func (r *personalBestRepository) GetVerifiedByUserID(userID uint) ([]models.Pers func (r *personalBestRepository) GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.PersonalBest, error) { var personalBests []models.PersonalBest err := r.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate). + Preload("User"). Order("date DESC, distance_type"). Find(&personalBests).Error if err != nil { @@ -117,6 +124,34 @@ func (r *personalBestRepository) GetByDateRange(userID uint, startDate, endDate return personalBests, nil } +// GetRecentPersonalBests возвращает последние личные рекорды +func (r *personalBestRepository) GetRecentPersonalBests(userID uint, limit int) ([]models.PersonalBest, error) { + var personalBests []models.PersonalBest + err := r.db.Where("user_id = ?", userID). + Preload("User"). + Order("created_at DESC"). + Limit(limit). + Find(&personalBests).Error + if err != nil { + return nil, err + } + return personalBests, nil +} + +// GetByEventName возвращает личные рекорды по названию события +func (r *personalBestRepository) GetByEventName(userID uint, eventName string) ([]models.PersonalBest, error) { + var personalBests []models.PersonalBest + err := r.db.Where("user_id = ? AND event_name LIKE ?", userID, "%"+eventName+"%"). + Preload("User"). + Order("date DESC"). + Find(&personalBests).Error + if err != nil { + return nil, err + } + return personalBests, nil +} + +// GetPersonalBestsSummary возвращает сводку лучших результатов по дистанциям // GetPersonalBestsSummary возвращает сводку лучших результатов по дистанциям func (r *personalBestRepository) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) { summary := &models.PersonalBestsSummary{} @@ -138,12 +173,16 @@ func (r *personalBestRepository) GetPersonalBestsSummary(userID uint) (*models.P switch distance { case models.Distance5K: summary.Best5K = best.Time + summary.Best5KPace = best.Pace case models.Distance10K: summary.Best10K = best.Time + summary.Best10KPace = best.Pace case models.DistanceHalf: summary.BestHalf = best.Time + summary.BestHalfPace = best.Pace case models.DistanceFull: summary.BestMarathon = best.Time + summary.BestMarathonPace = best.Pace } } } diff --git a/serv_nginx/api_bb/internal/routes/routes.go b/serv_nginx/api_bb/internal/routes/routes.go index 91e7c8b..9130fa9 100644 --- a/serv_nginx/api_bb/internal/routes/routes.go +++ b/serv_nginx/api_bb/internal/routes/routes.go @@ -137,6 +137,29 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { r.Patch("/verify", h.UserAchievementHandler().VerifyAchievement) }) }) + // Personal Best routes + r.Route("/personal-bests", func(r chi.Router) { + // CRUD operations + r.Post("/", h.PersonalBestHandler().CreatePersonalBest) + r.Get("/", h.PersonalBestHandler().GetUserPersonalBests) + r.Get("/recent", h.PersonalBestHandler().GetRecentPersonalBests) + r.Get("/summary", h.PersonalBestHandler().GetPersonalBestsSummary) + r.Post("/calculate-pace", h.PersonalBestHandler().CalculatePace) + + // Distance-specific routes + r.Route("/distance/{distanceType}", func(r chi.Router) { + r.Get("/", h.PersonalBestHandler().GetPersonalBestsByDistance) + r.Get("/best", h.PersonalBestHandler().GetBestByDistance) + }) + + // Individual personal best routes + r.Route("/{id}", func(r chi.Router) { + r.Get("/", h.PersonalBestHandler().GetPersonalBest) + r.Put("/", h.PersonalBestHandler().UpdatePersonalBest) + r.Delete("/", h.PersonalBestHandler().DeletePersonalBest) + r.Patch("/verify", h.PersonalBestHandler().VerifyPersonalBest) + }) + }) }) r.Route("/news", func(r chi.Router) { diff --git a/serv_nginx/api_bb/internal/service/personal_best_service.go b/serv_nginx/api_bb/internal/service/personal_best_service.go index f7c1797..4ec49d1 100644 --- a/serv_nginx/api_bb/internal/service/personal_best_service.go +++ b/serv_nginx/api_bb/internal/service/personal_best_service.go @@ -5,6 +5,7 @@ import ( "api_bb/internal/models" "api_bb/internal/repository" "fmt" + "time" "gorm.io/gorm" ) @@ -64,11 +65,110 @@ func (s *PersonalBestService) CreatePersonalBest(userID uint, req models.Persona return personalBest, nil } +// GetPersonalBestByID возвращает личный рекорд по ID +func (s *PersonalBestService) GetPersonalBestByID(id uint) (*models.PersonalBest, error) { + return s.pbRepo.GetByID(id) +} + // GetUserPersonalBests возвращает все личные рекорды пользователя func (s *PersonalBestService) GetUserPersonalBests(userID uint) ([]models.PersonalBest, error) { return s.pbRepo.GetByUserID(userID) } +// GetPersonalBestsByDistance возвращает личные рекорды по дистанции +func (s *PersonalBestService) GetPersonalBestsByDistance(userID uint, distanceType models.DistanceType) ([]models.PersonalBest, error) { + return s.pbRepo.GetByUserAndDistance(userID, distanceType) +} + +// GetBestByDistance возвращает лучший результат на дистанции +func (s *PersonalBestService) GetBestByDistance(userID uint, distanceType models.DistanceType) (*models.PersonalBest, error) { + return s.pbRepo.GetBestByDistance(userID, distanceType) +} + +// UpdatePersonalBest обновляет личный рекорд +func (s *PersonalBestService) UpdatePersonalBest(id uint, userID uint, req models.PersonalBestUpdateRequest) (*models.PersonalBest, error) { + // Получаем существующий рекорд + pb, err := s.pbRepo.GetByID(id) + if err != nil { + return nil, err + } + + // Проверяем, что рекорд принадлежит пользователю + if pb.UserID != userID { + return nil, gorm.ErrRecordNotFound + } + + // Обновляем поля + if req.DistanceType != "" { + pb.DistanceType = req.DistanceType + } + if req.Time != "" { + pb.Time = req.Time + // Пересчитываем темп при изменении времени + if req.Pace == "" { + calculatedPace, err := s.pbRepo.CalculatePace(req.Time, pb.DistanceType) + if err != nil { + return nil, err + } + pb.Pace = calculatedPace + } + } + if req.Pace != "" { + pb.Pace = req.Pace + } + if !req.Date.IsZero() { + pb.Date = req.Date + } + if req.EventName != "" { + pb.EventName = req.EventName + } + if req.Location != "" { + pb.Location = req.Location + } + pb.Verified = req.Verified + + if err := s.pbRepo.Update(pb); err != nil { + return nil, err + } + + return pb, nil +} + +// DeletePersonalBest удаляет личный рекорд +func (s *PersonalBestService) DeletePersonalBest(id uint, userID uint) error { + // Проверяем, что рекорд принадлежит пользователю + pb, err := s.pbRepo.GetByID(id) + if err != nil { + return err + } + + if pb.UserID != userID { + return gorm.ErrRecordNotFound + } + + return s.pbRepo.Delete(id) +} + +// GetVerifiedPersonalBests возвращает подтвержденные личные рекорды +func (s *PersonalBestService) GetVerifiedPersonalBests(userID uint) ([]models.PersonalBest, error) { + return s.pbRepo.GetVerifiedByUserID(userID) +} + +// GetPersonalBestsByDateRange возвращает личные рекорды за период +func (s *PersonalBestService) GetPersonalBestsByDateRange(userID uint, startDate, endDate time.Time) ([]models.PersonalBest, error) { + return s.pbRepo.GetByDateRange(userID, startDate, endDate) +} + +// GetRecentPersonalBests возвращает последние личные рекорды +func (s *PersonalBestService) GetRecentPersonalBests(userID uint, limit int) ([]models.PersonalBest, error) { + return s.pbRepo.GetRecentPersonalBests(userID, limit) +} + +// GetPersonalBestsByEvent возвращает личные рекорды по названию события +func (s *PersonalBestService) GetPersonalBestsByEvent(userID uint, eventName string) ([]models.PersonalBest, error) { + return s.pbRepo.GetByEventName(userID, eventName) +} + // GetPersonalBestsSummary возвращает сводку лучших результатов func (s *PersonalBestService) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) { return s.pbRepo.GetPersonalBestsSummary(userID) @@ -89,3 +189,8 @@ func (s *PersonalBestService) VerifyPersonalBest(id uint, userID uint) error { pb.Verified = true return s.pbRepo.Update(pb) } + +// CalculatePace вычисляет темп для времени и дистанции +func (s *PersonalBestService) CalculatePace(timeStr string, distanceType models.DistanceType) (string, error) { + return s.pbRepo.CalculatePace(timeStr, distanceType) +} diff --git a/serv_nginx/bbvue/src/stores/user.js b/serv_nginx/bbvue/src/stores/user.js index 1d9a3fa..30d04f7 100644 --- a/serv_nginx/bbvue/src/stores/user.js +++ b/serv_nginx/bbvue/src/stores/user.js @@ -65,6 +65,7 @@ export const useUserStore = defineStore('user', () => { return withStoreLoading(async () => { try { const response = await apiClient.get('/user/stats') + console.log("debug /user/stats " + response.data) userStats.value = response.data return { success: true, data: userStats.value } } catch (error) { @@ -95,6 +96,7 @@ export const useUserStore = defineStore('user', () => { return withStoreLoading(async () => { try { const response = await apiClient.get('/user/achievements') + console.log("debug /user/achievements " + response.data) userAchievements.value = response.data return { success: true, data: userAchievements.value } } catch (error) { @@ -154,6 +156,7 @@ export const useUserStore = defineStore('user', () => { return withStoreLoading(async () => { try { const response = await apiClient.get('/personal-bests') + console.log("debug /user/personal-bests " + response.data) personalBests.value = response.data return { success: true, data: personalBests.value } } catch (error) { @@ -189,6 +192,7 @@ export const useUserStore = defineStore('user', () => { return withStoreLoading(async () => { try { const response = await apiClient.get('/events/upcoming') + console.log("debug /events/upcoming " + response.data) upcomingEvents.value = response.data return { success: true, data: upcomingEvents.value } } catch (error) {