modified: serv_nginx/api_bb/internal/database/migrate.go

modified:   serv_nginx/api_bb/internal/handlers/handlers.go
	new file:   serv_nginx/api_bb/internal/handlers/personal_best_handler.go
	modified:   serv_nginx/api_bb/internal/models/personal_best.go
	modified:   serv_nginx/api_bb/internal/models/user_stats.go
	modified:   serv_nginx/api_bb/internal/repository/personal_best_repository.go
	modified:   serv_nginx/api_bb/internal/routes/routes.go
	modified:   serv_nginx/api_bb/internal/service/personal_best_service.go
	modified:   serv_nginx/bbvue/src/stores/user.js
personal bests add handler, rout, service, repository, logic and
migrations for
This commit is contained in:
2025-10-20 03:06:06 +05:00
parent 1d0d99e938
commit 402296b726
9 changed files with 708 additions and 14 deletions
@@ -24,15 +24,16 @@ func (d *Database) Migrate() error {
&models.Achievement{}, &models.Achievement{},
&models.Event{}, &models.Event{},
&models.EventRegistration{}, &models.EventRegistration{},
&models.PersonalBest{},
// Добавьте другие модели здесь // Добавьте другие модели здесь
} }
for _, model := range models { for _, model := range models {
modelName := getModelName(model) modelName := getModelName(model)
zapLogger.Debug("migrating model", zap.String("model", modelName)) zapLogger.Debug("migrating model", zap.String("model", modelName))
if err := d.DB.AutoMigrate(model); err != nil { 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.String("model", modelName),
zap.Error(err), zap.Error(err),
) )
@@ -48,16 +49,16 @@ func (d *Database) Migrate() error {
func (d *Database) MigrateModels(models ...interface{}) error { func (d *Database) MigrateModels(models ...interface{}) error {
zapLogger := logger.Get() zapLogger := logger.Get()
zapLogger.Info("starting migration for specific models", zapLogger.Info("starting migration for specific models",
zap.Int("model_count", len(models)), zap.Int("model_count", len(models)),
) )
for _, model := range models { for _, model := range models {
modelName := getModelName(model) modelName := getModelName(model)
zapLogger.Debug("migrating model", zap.String("model", modelName)) zapLogger.Debug("migrating model", zap.String("model", modelName))
if err := d.DB.AutoMigrate(model); err != nil { 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.String("model", modelName),
zap.Error(err), zap.Error(err),
) )
@@ -90,7 +91,9 @@ func getModelName(model interface{}) string {
return "Событие" return "Событие"
case *models.EventRegistration: case *models.EventRegistration:
return "Администрирование события" return "Администрирование события"
case *models.PersonalBest:
return "Персональные достижения"
default: default:
return "Unknown" return "Unknown"
} }
} }
@@ -22,6 +22,7 @@ type Handler struct {
userAchievementHandler *UserAchievementHandler userAchievementHandler *UserAchievementHandler
eventHandler *EventHandler eventHandler *EventHandler
eventRegistrationHandler *EventRegistrationHandler eventRegistrationHandler *EventRegistrationHandler
personalBestHandler *PersonalBestHandler
// Здесь будут добавлены другие обработчики // Здесь будут добавлены другие обработчики
// userHandler *UserHandler // userHandler *UserHandler
// eventHandler *EventHandler // eventHandler *EventHandler
@@ -39,6 +40,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
userAchievemenRepo := repository.NewAchievementRepository(db) userAchievemenRepo := repository.NewAchievementRepository(db)
eventRepo := repository.NewEventRepository(db) eventRepo := repository.NewEventRepository(db)
eventRegistrationRepo := repository.NewEventRegistrationRepository(db) eventRegistrationRepo := repository.NewEventRegistrationRepository(db)
personalBestRepo := repository.NewPersonalBestRepository(db)
// Initialize logger // Initialize logger
baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер
@@ -55,6 +57,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
achievementService := service.NewAchievementService(userAchievemenRepo) achievementService := service.NewAchievementService(userAchievemenRepo)
eventRegistrationService := service.NewEventRegistrationService(eventRegistrationRepo, eventRepo, baseLogger) eventRegistrationService := service.NewEventRegistrationService(eventRegistrationRepo, eventRepo, baseLogger)
eventService := service.NewEventService(eventRepo, eventRegistrationRepo, baseLogger) eventService := service.NewEventService(eventRepo, eventRegistrationRepo, baseLogger)
personalBestService := service.NewPersonalBestService(personalBestRepo, userStatsService)
// Инициализация обработчиков // Инициализация обработчиков
healthHandler := NewHealthHandler() healthHandler := NewHealthHandler()
@@ -68,6 +71,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
userAchievementHandler := NewUserAchievementHandler(*achievementService) userAchievementHandler := NewUserAchievementHandler(*achievementService)
eventHandler := NewEventHandler(eventService) eventHandler := NewEventHandler(eventService)
eventRegistrationHandler := NewEventRegistrationHandler(eventRegistrationService) eventRegistrationHandler := NewEventRegistrationHandler(eventRegistrationService)
personalBestHandler := NewPersonalBestHandler(*personalBestService)
return &Handler{ return &Handler{
healthHandler: healthHandler, healthHandler: healthHandler,
@@ -81,10 +85,15 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
userAchievementHandler: userAchievementHandler, userAchievementHandler: userAchievementHandler,
eventHandler: eventHandler, eventHandler: eventHandler,
eventRegistrationHandler: eventRegistrationHandler, eventRegistrationHandler: eventRegistrationHandler,
personalBestHandler: personalBestHandler,
} }
} }
// Геттеры для обработчиков (опционально, для удобства) // Геттеры для обработчиков (опционально, для удобства)
func (h *Handler) PersonalBestHandler() *PersonalBestHandler {
return h.personalBestHandler
}
func (h *Handler) EventHandler() *EventHandler { func (h *Handler) EventHandler() *EventHandler {
return h.eventHandler return h.eventHandler
} }
@@ -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,
})
}
@@ -70,4 +70,16 @@ type PersonalBestUpdateRequest struct {
EventName string `json:"event_name" validate:"omitempty,max=255"` EventName string `json:"event_name" validate:"omitempty,max=255"`
Location string `json:"location" validate:"omitempty,max=255"` Location string `json:"location" validate:"omitempty,max=255"`
Verified bool `json:"verified"` 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"`
} }
@@ -58,11 +58,4 @@ type UserStatsResponse struct {
WeeklyDistance float64 `json:"weekly_distance"` WeeklyDistance float64 `json:"weekly_distance"`
MonthlyDistance float64 `json:"monthly_distance"` MonthlyDistance float64 `json:"monthly_distance"`
PersonalBests PersonalBestsSummary `json:"personal_bests"` 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"`
} }
@@ -4,9 +4,9 @@ package repository
import ( import (
"time" "time"
"gorm.io/gorm"
"api_bb/internal/models" "api_bb/internal/models"
"api_bb/pkg/utils" "api_bb/pkg/utils"
"gorm.io/gorm"
) )
type PersonalBestRepository interface { type PersonalBestRepository interface {
@@ -22,6 +22,8 @@ type PersonalBestRepository interface {
GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error)
ExistsBetterTime(userID uint, distanceType models.DistanceType, time string) (bool, error) ExistsBetterTime(userID uint, distanceType models.DistanceType, time string) (bool, error)
CalculatePace(timeStr string, distanceType models.DistanceType) (string, 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 { 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) { func (r *personalBestRepository) GetByUserID(userID uint) ([]models.PersonalBest, error) {
var personalBests []models.PersonalBest var personalBests []models.PersonalBest
err := r.db.Where("user_id = ?", userID). err := r.db.Where("user_id = ?", userID).
Preload("User").
Order("distance_type, time"). Order("distance_type, time").
Find(&personalBests).Error Find(&personalBests).Error
if err != nil { 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) { func (r *personalBestRepository) GetByUserAndDistance(userID uint, distanceType models.DistanceType) ([]models.PersonalBest, error) {
var personalBests []models.PersonalBest var personalBests []models.PersonalBest
err := r.db.Where("user_id = ? AND distance_type = ?", userID, distanceType). err := r.db.Where("user_id = ? AND distance_type = ?", userID, distanceType).
Preload("User").
Order("time"). Order("time").
Find(&personalBests).Error Find(&personalBests).Error
if err != nil { 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) { func (r *personalBestRepository) GetBestByDistance(userID uint, distanceType models.DistanceType) (*models.PersonalBest, error) {
var personalBest models.PersonalBest var personalBest models.PersonalBest
err := r.db.Where("user_id = ? AND distance_type = ?", userID, distanceType). err := r.db.Where("user_id = ? AND distance_type = ?", userID, distanceType).
Preload("User").
Order("time"). Order("time").
First(&personalBest).Error First(&personalBest).Error
if err != nil { if err != nil {
@@ -97,6 +102,7 @@ func (r *personalBestRepository) Delete(id uint) error {
func (r *personalBestRepository) GetVerifiedByUserID(userID uint) ([]models.PersonalBest, error) { func (r *personalBestRepository) GetVerifiedByUserID(userID uint) ([]models.PersonalBest, error) {
var personalBests []models.PersonalBest var personalBests []models.PersonalBest
err := r.db.Where("user_id = ? AND verified = ?", userID, true). err := r.db.Where("user_id = ? AND verified = ?", userID, true).
Preload("User").
Order("distance_type, time"). Order("distance_type, time").
Find(&personalBests).Error Find(&personalBests).Error
if err != nil { 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) { func (r *personalBestRepository) GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.PersonalBest, error) {
var personalBests []models.PersonalBest var personalBests []models.PersonalBest
err := r.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate). err := r.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate).
Preload("User").
Order("date DESC, distance_type"). Order("date DESC, distance_type").
Find(&personalBests).Error Find(&personalBests).Error
if err != nil { if err != nil {
@@ -117,6 +124,34 @@ func (r *personalBestRepository) GetByDateRange(userID uint, startDate, endDate
return personalBests, nil 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 возвращает сводку лучших результатов по дистанциям // GetPersonalBestsSummary возвращает сводку лучших результатов по дистанциям
func (r *personalBestRepository) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) { func (r *personalBestRepository) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) {
summary := &models.PersonalBestsSummary{} summary := &models.PersonalBestsSummary{}
@@ -138,12 +173,16 @@ func (r *personalBestRepository) GetPersonalBestsSummary(userID uint) (*models.P
switch distance { switch distance {
case models.Distance5K: case models.Distance5K:
summary.Best5K = best.Time summary.Best5K = best.Time
summary.Best5KPace = best.Pace
case models.Distance10K: case models.Distance10K:
summary.Best10K = best.Time summary.Best10K = best.Time
summary.Best10KPace = best.Pace
case models.DistanceHalf: case models.DistanceHalf:
summary.BestHalf = best.Time summary.BestHalf = best.Time
summary.BestHalfPace = best.Pace
case models.DistanceFull: case models.DistanceFull:
summary.BestMarathon = best.Time summary.BestMarathon = best.Time
summary.BestMarathonPace = best.Pace
} }
} }
} }
@@ -137,6 +137,29 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
r.Patch("/verify", h.UserAchievementHandler().VerifyAchievement) 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) { r.Route("/news", func(r chi.Router) {
@@ -5,6 +5,7 @@ import (
"api_bb/internal/models" "api_bb/internal/models"
"api_bb/internal/repository" "api_bb/internal/repository"
"fmt" "fmt"
"time"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -64,11 +65,110 @@ func (s *PersonalBestService) CreatePersonalBest(userID uint, req models.Persona
return personalBest, nil return personalBest, nil
} }
// GetPersonalBestByID возвращает личный рекорд по ID
func (s *PersonalBestService) GetPersonalBestByID(id uint) (*models.PersonalBest, error) {
return s.pbRepo.GetByID(id)
}
// GetUserPersonalBests возвращает все личные рекорды пользователя // GetUserPersonalBests возвращает все личные рекорды пользователя
func (s *PersonalBestService) GetUserPersonalBests(userID uint) ([]models.PersonalBest, error) { func (s *PersonalBestService) GetUserPersonalBests(userID uint) ([]models.PersonalBest, error) {
return s.pbRepo.GetByUserID(userID) 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 возвращает сводку лучших результатов // GetPersonalBestsSummary возвращает сводку лучших результатов
func (s *PersonalBestService) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) { func (s *PersonalBestService) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) {
return s.pbRepo.GetPersonalBestsSummary(userID) return s.pbRepo.GetPersonalBestsSummary(userID)
@@ -89,3 +189,8 @@ func (s *PersonalBestService) VerifyPersonalBest(id uint, userID uint) error {
pb.Verified = true pb.Verified = true
return s.pbRepo.Update(pb) return s.pbRepo.Update(pb)
} }
// CalculatePace вычисляет темп для времени и дистанции
func (s *PersonalBestService) CalculatePace(timeStr string, distanceType models.DistanceType) (string, error) {
return s.pbRepo.CalculatePace(timeStr, distanceType)
}
+4
View File
@@ -65,6 +65,7 @@ export const useUserStore = defineStore('user', () => {
return withStoreLoading(async () => { return withStoreLoading(async () => {
try { try {
const response = await apiClient.get('/user/stats') const response = await apiClient.get('/user/stats')
console.log("debug /user/stats " + response.data)
userStats.value = response.data userStats.value = response.data
return { success: true, data: userStats.value } return { success: true, data: userStats.value }
} catch (error) { } catch (error) {
@@ -95,6 +96,7 @@ export const useUserStore = defineStore('user', () => {
return withStoreLoading(async () => { return withStoreLoading(async () => {
try { try {
const response = await apiClient.get('/user/achievements') const response = await apiClient.get('/user/achievements')
console.log("debug /user/achievements " + response.data)
userAchievements.value = response.data userAchievements.value = response.data
return { success: true, data: userAchievements.value } return { success: true, data: userAchievements.value }
} catch (error) { } catch (error) {
@@ -154,6 +156,7 @@ export const useUserStore = defineStore('user', () => {
return withStoreLoading(async () => { return withStoreLoading(async () => {
try { try {
const response = await apiClient.get('/personal-bests') const response = await apiClient.get('/personal-bests')
console.log("debug /user/personal-bests " + response.data)
personalBests.value = response.data personalBests.value = response.data
return { success: true, data: personalBests.value } return { success: true, data: personalBests.value }
} catch (error) { } catch (error) {
@@ -189,6 +192,7 @@ export const useUserStore = defineStore('user', () => {
return withStoreLoading(async () => { return withStoreLoading(async () => {
try { try {
const response = await apiClient.get('/events/upcoming') const response = await apiClient.get('/events/upcoming')
console.log("debug /events/upcoming " + response.data)
upcomingEvents.value = response.data upcomingEvents.value = response.data
return { success: true, data: upcomingEvents.value } return { success: true, data: upcomingEvents.value }
} catch (error) { } catch (error) {