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:
2025-10-17 06:25:06 +05:00
parent b19ce8fdfe
commit c5bf8583e4
4 changed files with 408 additions and 1 deletions
@@ -14,7 +14,7 @@ type UserStats struct {
TotalTime int `json:"total_time" gorm:"default:0"` // Общее время в минутах TotalTime int `json:"total_time" gorm:"default:0"` // Общее время в минутах
AvgPace string `json:"avg_pace" gorm:"size:20"` // Средний темп AvgPace string `json:"avg_pace" gorm:"size:20"` // Средний темп
WorkoutsCount int `json:"workouts_count" gorm:"default:0"` // Количество тренировок 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"` // Самая длинная серия LongestStreak int `json:"longest_streak" gorm:"default:0"` // Самая длинная серия
WeeklyDistance float64 `json:"weekly_distance" gorm:"type:decimal(8,2);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"` // Пробег за месяц 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
}
+27
View File
@@ -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)
}