diff --git a/serv_nginx/api_bb/internal/models/user_stats.go b/serv_nginx/api_bb/internal/models/user_stats.go index b17da52..81c727d 100644 --- a/serv_nginx/api_bb/internal/models/user_stats.go +++ b/serv_nginx/api_bb/internal/models/user_stats.go @@ -14,7 +14,7 @@ type UserStats struct { TotalTime int `json:"total_time" gorm:"default:0"` // Общее время в минутах AvgPace string `json:"avg_pace" gorm:"size:20"` // Средний темп WorkoutsCount int `json:"workouts_count" gorm:"default:0"` // Количество тренировок - CurrentStreak int `json:"current_streak" gorm:"default:0"` // Текущая серия дней подряд + CurrentStreak int `json:"current_streak" gorm:"default:0"` // Текущая серия дней подряд LongestStreak int `json:"longest_streak" gorm:"default:0"` // Самая длинная серия WeeklyDistance float64 `json:"weekly_distance" gorm:"type:decimal(8,2);default:0"` // Пробег за неделю MonthlyDistance float64 `json:"monthly_distance" gorm:"type:decimal(8,2);default:0"` // Пробег за месяц diff --git a/serv_nginx/api_bb/internal/repository/personal_best_repository.go b/serv_nginx/api_bb/internal/repository/personal_best_repository.go new file mode 100644 index 0000000..17749f4 --- /dev/null +++ b/serv_nginx/api_bb/internal/repository/personal_best_repository.go @@ -0,0 +1,199 @@ +// repositories/personal_best_repository.go +package repository + +import ( + "time" + + "gorm.io/gorm" + "api_bb/internal/models" + "api_bb/pkg/utils" +) + +type PersonalBestRepository interface { + Create(personalBest *models.PersonalBest) error + GetByID(id uint) (*models.PersonalBest, error) + GetByUserID(userID uint) ([]models.PersonalBest, error) + GetByUserAndDistance(userID uint, distanceType models.DistanceType) ([]models.PersonalBest, error) + GetBestByDistance(userID uint, distanceType models.DistanceType) (*models.PersonalBest, error) + Update(personalBest *models.PersonalBest) error + Delete(id uint) error + GetVerifiedByUserID(userID uint) ([]models.PersonalBest, error) + GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.PersonalBest, error) + GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) + ExistsBetterTime(userID uint, distanceType models.DistanceType, time string) (bool, error) +} + +type personalBestRepository struct { + db *gorm.DB +} + +func NewPersonalBestRepository(db *gorm.DB) PersonalBestRepository { + return &personalBestRepository{db: db} +} + +// Create создает новый личный рекорд +func (r *personalBestRepository) Create(personalBest *models.PersonalBest) error { + return r.db.Create(personalBest).Error +} + +// GetByID возвращает личный рекорд по ID +func (r *personalBestRepository) GetByID(id uint) (*models.PersonalBest, error) { + var personalBest models.PersonalBest + err := r.db.Preload("User").First(&personalBest, id).Error + if err != nil { + return nil, err + } + return &personalBest, nil +} + +// GetByUserID возвращает все личные рекорды пользователя +func (r *personalBestRepository) GetByUserID(userID uint) ([]models.PersonalBest, error) { + var personalBests []models.PersonalBest + err := r.db.Where("user_id = ?", userID). + Order("distance_type, time"). + Find(&personalBests).Error + if err != nil { + return nil, err + } + return personalBests, nil +} + +// GetByUserAndDistance возвращает личные рекорды пользователя по дистанции +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). + Order("time"). + Find(&personalBests).Error + if err != nil { + return nil, err + } + return personalBests, nil +} + +// GetBestByDistance возвращает лучший результат пользователя на дистанции +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). + Order("time"). + First(&personalBest).Error + if err != nil { + return nil, err + } + return &personalBest, nil +} + +// Update обновляет личный рекорд +func (r *personalBestRepository) Update(personalBest *models.PersonalBest) error { + return r.db.Save(personalBest).Error +} + +// Delete удаляет личный рекорд +func (r *personalBestRepository) Delete(id uint) error { + return r.db.Delete(&models.PersonalBest{}, id).Error +} + +// GetVerifiedByUserID возвращает подтвержденные личные рекорды пользователя +func (r *personalBestRepository) GetVerifiedByUserID(userID uint) ([]models.PersonalBest, error) { + var personalBests []models.PersonalBest + err := r.db.Where("user_id = ? AND verified = ?", userID, true). + Order("distance_type, time"). + Find(&personalBests).Error + if err != nil { + return nil, err + } + return personalBests, nil +} + +// GetByDateRange возвращает личные рекорды за период времени +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). + Order("date DESC, distance_type"). + Find(&personalBests).Error + if err != nil { + return nil, err + } + return personalBests, nil +} + +// GetPersonalBestsSummary возвращает сводку лучших результатов по дистанциям +func (r *personalBestRepository) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) { + summary := &models.PersonalBestsSummary{} + + // Получаем лучший результат для каждой дистанции + distances := []models.DistanceType{ + models.Distance5K, + models.Distance10K, + models.DistanceHalf, + models.DistanceFull, + } + + for _, distance := range distances { + best, err := r.GetBestByDistance(userID, distance) + if err != nil && err != gorm.ErrRecordNotFound { + return nil, err + } + if best != nil { + switch distance { + case models.Distance5K: + summary.Best5K = best.Time + case models.Distance10K: + summary.Best10K = best.Time + case models.DistanceHalf: + summary.BestHalf = best.Time + case models.DistanceFull: + summary.BestMarathon = best.Time + } + } + } + + return summary, nil +} + +// ExistsBetterTime проверяет, есть ли у пользователя уже лучший результат на этой дистанции +func (r *personalBestRepository) ExistsBetterTime(userID uint, distanceType models.DistanceType, time string) (bool, error) { + var count int64 + err := r.db.Model(&models.PersonalBest{}). + Where("user_id = ? AND distance_type = ? AND time < ?", userID, distanceType, time). + Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +// CalculatePace вычисляет темп на основе времени и дистанции +func (r *personalBestRepository) CalculatePace(timeStr string, distanceType models.DistanceType) (string, error) { + // Парсим время из строки "HH:MM:SS" + t, err := time.Parse("15:04:05", timeStr) + if err != nil { + return "", err + } + + // Преобразуем в секунды + totalSeconds := t.Hour()*3600 + t.Minute()*60 + t.Second() + + // Определяем дистанцию в метрах + var distanceMeters float64 + switch distanceType { + case models.Distance5K: + distanceMeters = 5000 + case models.Distance10K: + distanceMeters = 10000 + case models.DistanceHalf: + distanceMeters = 21097.5 // 21.0975 km + case models.DistanceFull: + distanceMeters = 42195 // 42.195 km + default: + return "", nil // Для других дистанций не вычисляем темп + } + + // Вычисляем темп в секундах на километр + paceSecondsPerKm := float64(totalSeconds) / (distanceMeters / 1000) + + // Форматируем темп в "MM:SS" + minutes := int(paceSecondsPerKm) / 60 + seconds := int(paceSecondsPerKm) % 60 + + return utils.FormatPace(minutes, seconds), nil +} diff --git a/serv_nginx/api_bb/internal/repository/user_stats_repository.go b/serv_nginx/api_bb/internal/repository/user_stats_repository.go new file mode 100644 index 0000000..3b9872b --- /dev/null +++ b/serv_nginx/api_bb/internal/repository/user_stats_repository.go @@ -0,0 +1,181 @@ +// repositories/user_stats_repository.go +package repository + +import ( + "time" + + "gorm.io/gorm" + "api_bb/internal/models" + "api_bb/pkg/utils" +) + +type UserStatsRepository interface { + Create(userStats *models.UserStats) error + GetByID(id uint) (*models.UserStats, error) + GetByUserID(userID uint) (*models.UserStats, error) + Update(userStats *models.UserStats) error + Delete(id uint) error + UpdateStreaks(userID uint, lastWorkout time.Time) error + UpdateWeeklyDistance(userID uint, distance float64) error + UpdateMonthlyDistance(userID uint, distance float64) error + IncrementWorkouts(userID uint, distance float64, duration int) error + UpdatePersonalBest(userID uint, distanceType string, time string) error + GetUserStatsResponse(userID uint) (*models.UserStatsResponse, error) +} + +type userStatsRepository struct { + db *gorm.DB +} + +func NewUserStatsRepository(db *gorm.DB) UserStatsRepository { + return &userStatsRepository{db: db} +} + +// Create создает новую статистику пользователя +func (r *userStatsRepository) Create(userStats *models.UserStats) error { + return r.db.Create(userStats).Error +} + +// GetByID возвращает статистику по ID +func (r *userStatsRepository) GetByID(id uint) (*models.UserStats, error) { + var userStats models.UserStats + err := r.db.First(&userStats, id).Error + if err != nil { + return nil, err + } + return &userStats, nil +} + +// GetByUserID возвращает статистику по ID пользователя +func (r *userStatsRepository) GetByUserID(userID uint) (*models.UserStats, error) { + var userStats models.UserStats + err := r.db.Where("user_id = ?", userID).First(&userStats).Error + if err != nil { + return nil, err + } + return &userStats, nil +} + +// Update обновляет статистику пользователя +func (r *userStatsRepository) Update(userStats *models.UserStats) error { + return r.db.Save(userStats).Error +} + +// Delete удаляет статистику пользователя +func (r *userStatsRepository) Delete(id uint) error { + return r.db.Delete(&models.UserStats{}, id).Error +} + +// UpdateStreaks обновляет серии тренировок +func (r *userStatsRepository) UpdateStreaks(userID uint, lastWorkout time.Time) error { + userStats, err := r.GetByUserID(userID) + if err != nil { + return err + } + + // Проверяем, была ли тренировка вчера + yesterday := time.Now().AddDate(0, 0, -1) + if userStats.LastWorkout.Format("2006-01-02") == yesterday.Format("2006-01-02") { + // Продолжаем серию + userStats.CurrentStreak++ + } else if userStats.LastWorkout.Format("2006-01-02") != time.Now().Format("2006-01-02") { + // Сбрасываем серию, если не было тренировки сегодня или вчера + userStats.CurrentStreak = 1 + } + + // Обновляем самую длинную серию + if userStats.CurrentStreak > userStats.LongestStreak { + userStats.LongestStreak = userStats.CurrentStreak + } + + userStats.LastWorkout = lastWorkout + + return r.Update(userStats) +} + +// UpdateWeeklyDistance обновляет недельный пробег +func (r *userStatsRepository) UpdateWeeklyDistance(userID uint, distance float64) error { + return r.db.Model(&models.UserStats{}). + Where("user_id = ?", userID). + Update("weekly_distance", gorm.Expr("weekly_distance + ?", distance)). + Error +} + +// UpdateMonthlyDistance обновляет месячный пробег +func (r *userStatsRepository) UpdateMonthlyDistance(userID uint, distance float64) error { + return r.db.Model(&models.UserStats{}). + Where("user_id = ?", userID). + Update("monthly_distance", gorm.Expr("monthly_distance + ?", distance)). + Error +} + +// IncrementWorkouts увеличивает счетчик тренировок и обновляет общие показатели +func (r *userStatsRepository) IncrementWorkouts(userID uint, distance float64, duration int) error { + userStats, err := r.GetByUserID(userID) + if err != nil { + return err + } + + // Обновляем общие показатели + userStats.WorkoutsCount++ + userStats.TotalDistance += distance + userStats.TotalTime += duration + + // Пересчитываем средний темп (в минутах на км) + if userStats.TotalDistance > 0 { + avgPaceMinPerKm := float64(userStats.TotalTime) / userStats.TotalDistance + minutes := int(avgPaceMinPerKm) + seconds := int((avgPaceMinPerKm - float64(minutes)) * 60) + userStats.AvgPace = utils.FormatPace(minutes, seconds) + } + + return r.Update(userStats) +} + +// UpdatePersonalBest обновляет личный рекорд +func (r *userStatsRepository) UpdatePersonalBest(userID uint, distanceType string, time string) error { + updateField := "" + switch distanceType { + case "5k": + updateField = "best_5k" + case "10k": + updateField = "best_10k" + case "half": + updateField = "best_half" + case "marathon": + updateField = "best_marathon" + default: + return nil + } + + return r.db.Model(&models.UserStats{}). + Where("user_id = ?", userID). + Update(updateField, time). + Error +} + +// GetUserStatsResponse возвращает статистику в формате DTO +func (r *userStatsRepository) GetUserStatsResponse(userID uint) (*models.UserStatsResponse, error) { + userStats, err := r.GetByUserID(userID) + if err != nil { + return nil, err + } + + return &models.UserStatsResponse{ + TotalDistance: userStats.TotalDistance, + TotalTime: userStats.TotalTime, + AvgPace: userStats.AvgPace, + WorkoutsCount: userStats.WorkoutsCount, + CurrentStreak: userStats.CurrentStreak, + LongestStreak: userStats.LongestStreak, + WeeklyDistance: userStats.WeeklyDistance, + MonthlyDistance: userStats.MonthlyDistance, + PersonalBests: models.PersonalBestsSummary{ + Best5K: userStats.Best5K, + Best10K: userStats.Best10K, + BestHalf: userStats.BestHalf, + BestMarathon: userStats.BestMarathon, + }, + }, nil +} + diff --git a/serv_nginx/api_bb/pkg/utils/formatTime.go b/serv_nginx/api_bb/pkg/utils/formatTime.go new file mode 100644 index 0000000..e35c536 --- /dev/null +++ b/serv_nginx/api_bb/pkg/utils/formatTime.go @@ -0,0 +1,27 @@ +package utils + +// formatPace форматирует темп в строку "MM:SS" +func FormatPace(minutes, seconds int) string { + if seconds >= 60 { + minutes += seconds / 60 + seconds = seconds % 60 + } + return FormatTwoDigits(minutes) + ":" + FormatTwoDigits(seconds) +} + +// formatTwoDigits форматирует число в двузначную строку +func FormatTwoDigits(num int) string { + if num < 10 { + return "0" + string(rune(num+'0')) + } + return string(rune(num/10+'0')) + string(rune(num%10+'0')) +} + +// formatTime форматирует время в строку "MM:SS" +func FormatTime(minutes, seconds int) string { + if seconds >= 60 { + minutes += seconds / 60 + seconds = seconds % 60 + } + return FormatTwoDigits(minutes) + ":" + FormatTwoDigits(seconds) +}