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:
@@ -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