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,6 +24,7 @@ func (d *Database) Migrate() error {
&models.Achievement{},
&models.Event{},
&models.EventRegistration{},
&models.PersonalBest{},
// Добавьте другие модели здесь
}
@@ -90,6 +91,8 @@ 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,
})
}
@@ -71,3 +71,15 @@ type PersonalBestUpdateRequest struct {
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"`
}
@@ -59,10 +59,3 @@ type UserStatsResponse struct {
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)
}
+4
View File
@@ -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) {