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:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user