modified: serv_nginx/api_bb/internal/models/user_stats.go
new file: serv_nginx/api_bb/internal/repository/personal_best_repository.go new file: serv_nginx/api_bb/internal/repository/user_stats_repository.go new file: serv_nginx/api_bb/pkg/utils/formatTime.go add stats, personal_best repositories
This commit is contained in:
@@ -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"` // Пробег за месяц
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user