modified: serv_nginx/api_bb/internal/repository/user_stats_repository.go

new file:   serv_nginx/api_bb/internal/scripts/migrate_existing_users.go
	modified:   serv_nginx/api_bb/internal/service/auth_service.go
	modified:   serv_nginx/api_bb/internal/service/user_stats_service.go
	modified:   serv_nginx/bbvue/src/views/Achievements.vue
	modified:   serv_nginx/bbvue/src/views/Reviews.vue
	modified:   serv_nginx/bbvue/src/views/Training.vue
fix bag with no stats into table
This commit is contained in:
2025-10-19 06:33:58 +05:00
parent 693ece204d
commit 15c59b2f55
7 changed files with 147 additions and 34 deletions
@@ -4,9 +4,9 @@ package repository
import ( import (
"time" "time"
"gorm.io/gorm"
"api_bb/internal/models" "api_bb/internal/models"
"api_bb/pkg/utils" "api_bb/pkg/utils"
"gorm.io/gorm"
) )
type UserStatsRepository interface { type UserStatsRepository interface {
@@ -21,6 +21,7 @@ type UserStatsRepository interface {
IncrementWorkouts(userID uint, distance float64, duration int) error IncrementWorkouts(userID uint, distance float64, duration int) error
UpdatePersonalBest(userID uint, distanceType string, time string) error UpdatePersonalBest(userID uint, distanceType string, time string) error
GetUserStatsResponse(userID uint) (*models.UserStatsResponse, error) GetUserStatsResponse(userID uint) (*models.UserStatsResponse, error)
GetByUserIDOrCreate(userID uint) (*models.UserStats, error)
} }
type userStatsRepository struct { type userStatsRepository struct {
@@ -31,6 +32,39 @@ func NewUserStatsRepository(db *gorm.DB) UserStatsRepository {
return &userStatsRepository{db: db} return &userStatsRepository{db: db}
} }
// GetByUserIDOrCreate возвращает статистику по ID пользователя или создает новую
func (r *userStatsRepository) GetByUserIDOrCreate(userID uint) (*models.UserStats, error) {
userStats, err := r.GetByUserID(userID)
if err != nil {
if err == gorm.ErrRecordNotFound {
// Создаем новую статистику
newStats := &models.UserStats{
UserID: userID,
TotalDistance: 0,
TotalTime: 0,
AvgPace: "0:00",
WorkoutsCount: 0,
CurrentStreak: 0,
LongestStreak: 0,
WeeklyDistance: 0,
MonthlyDistance: 0,
Best5K: "",
Best10K: "",
BestHalf: "",
BestMarathon: "",
LastWorkout: time.Time{},
}
if err := r.Create(newStats); err != nil {
return nil, err
}
return newStats, nil
}
return nil, err
}
return userStats, nil
}
// Create создает новую статистику пользователя // Create создает новую статистику пользователя
func (r *userStatsRepository) Create(userStats *models.UserStats) error { func (r *userStatsRepository) Create(userStats *models.UserStats) error {
return r.db.Create(userStats).Error return r.db.Create(userStats).Error
@@ -156,7 +190,7 @@ func (r *userStatsRepository) UpdatePersonalBest(userID uint, distanceType strin
// GetUserStatsResponse возвращает статистику в формате DTO // GetUserStatsResponse возвращает статистику в формате DTO
func (r *userStatsRepository) GetUserStatsResponse(userID uint) (*models.UserStatsResponse, error) { func (r *userStatsRepository) GetUserStatsResponse(userID uint) (*models.UserStatsResponse, error) {
userStats, err := r.GetByUserID(userID) userStats, err := r.GetByUserIDOrCreate(userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -178,4 +212,3 @@ func (r *userStatsRepository) GetUserStatsResponse(userID uint) (*models.UserSta
}, },
}, nil }, nil
} }
@@ -0,0 +1,74 @@
// scripts/migrate_existing_users.go
package main
import (
"api_bb/internal/models"
"api_bb/internal/repository"
"api_bb/pkg/logger"
"gorm.io/gorm"
"go.uber.org/zap"
)
func MigrateExistingUsers(db *gorm.DB) error {
log := logger.NewWrapper(logger.Get().With(zap.String("script", "migrate_existing_users")))
userRepo := repository.NewUserRepository(db)
userStatsRepo := repository.NewUserStatsRepository(db)
// Получаем всех пользователей
users, err := userRepo.FindAll()
if err != nil {
return err
}
log.Info("starting migration for existing users",
zap.Int("total_users", len(users)))
successCount := 0
for _, user := range users {
// Проверяем, есть ли уже статистика
_, err := userStatsRepo.GetByUserID(user.ID)
if err == gorm.ErrRecordNotFound {
// Создаем статистику
userStats := &models.UserStats{
UserID: user.ID,
TotalDistance: 0,
TotalTime: 0,
AvgPace: "0:00",
WorkoutsCount: 0,
CurrentStreak: 0,
LongestStreak: 0,
WeeklyDistance: 0,
MonthlyDistance: 0,
Best5K: "",
Best10K: "",
BestHalf: "",
BestMarathon: "",
LastWorkout: user.CreatedAt, // Используем дату создания как последнюю тренировку
}
if err := userStatsRepo.Create(userStats); err != nil {
log.Error("failed to create stats for user",
zap.Uint("user_id", user.ID),
zap.Error(err))
continue
}
successCount++
log.Info("created stats for user",
zap.Uint("user_id", user.ID),
zap.String("email", user.Email))
} else if err != nil {
log.Error("error checking stats for user",
zap.Uint("user_id", user.ID),
zap.Error(err))
}
}
log.Info("migration completed",
zap.Int("successful_creations", successCount),
zap.Int("total_users", len(users)))
return nil
}
@@ -56,11 +56,13 @@ func (s *authService) Register(user *models.User) error {
return err return err
} }
s.logger.Info("User registered successfully", s.logger.Info("User registered successfully",
zap.Uint("user_id", user.ID), zap.Uint("user_id", user.ID),
zap.String("email", user.Email), zap.String("email", user.Email),
) )
return nil return nil
} }
@@ -21,8 +21,8 @@ type UserStatsService interface {
} }
type userStatsService struct { type userStatsService struct {
logger logger.LoggerInterface logger logger.LoggerInterface
userStatsRepo repository.UserStatsRepository userStatsRepo repository.UserStatsRepository
} }
func NewUserStatsService(userStatsRepo repository.UserStatsRepository) UserStatsService { func NewUserStatsService(userStatsRepo repository.UserStatsRepository) UserStatsService {
@@ -64,15 +64,14 @@ func (s *userStatsService) UpdatePersonalBest(userID uint, distanceType string,
zap.String("time", time), zap.String("time", time),
) )
// Проверяем существование статистики пользователя // Используем GetByUserIDOrCreate вместо проверки существования
_, err := s.userStatsRepo.GetByUserID(userID) _, err := s.userStatsRepo.GetByUserIDOrCreate(userID)
if err != nil { if err != nil {
s.logger.Warn("user stats not found, creating new stats", s.logger.Error("failed to get or create user stats",
zap.Uint("user_id", userID), zap.Uint("user_id", userID),
zap.Error(err),
) )
if err := s.CreateUserStats(userID); err != nil { return err
return err
}
} }
if err := s.userStatsRepo.UpdatePersonalBest(userID, distanceType, time); err != nil { if err := s.userStatsRepo.UpdatePersonalBest(userID, distanceType, time); err != nil {
@@ -101,15 +100,14 @@ func (s *userStatsService) IncrementWorkout(userID uint, distance float64, durat
zap.Int("duration", duration), zap.Int("duration", duration),
) )
// Проверяем существование статистики пользователя // Используем GetByUserIDOrCreate для гарантии существования статистики
_, err := s.userStatsRepo.GetByUserID(userID) _, err := s.userStatsRepo.GetByUserIDOrCreate(userID)
if err != nil { if err != nil {
s.logger.Warn("user stats not found, creating new stats", s.logger.Error("failed to get or create user stats",
zap.Uint("user_id", userID), zap.Uint("user_id", userID),
zap.Error(err),
) )
if err := s.CreateUserStats(userID); err != nil { return err
return err
}
} }
// Обновляем серии тренировок // Обновляем серии тренировок
@@ -226,20 +224,20 @@ func (s *userStatsService) CreateUserStats(userID uint) error {
) )
userStats := &models.UserStats{ userStats := &models.UserStats{
UserID: userID, UserID: userID,
TotalDistance: 0, TotalDistance: 0,
TotalTime: 0, TotalTime: 0,
AvgPace: "0:00", AvgPace: "0:00",
WorkoutsCount: 0, WorkoutsCount: 0,
CurrentStreak: 0, CurrentStreak: 0,
LongestStreak: 0, LongestStreak: 0,
WeeklyDistance: 0, WeeklyDistance: 0,
MonthlyDistance: 0, MonthlyDistance: 0,
Best5K: "", Best5K: "",
Best10K: "", Best10K: "",
BestHalf: "", BestHalf: "",
BestMarathon: "", BestMarathon: "",
LastWorkout: time.Time{}, LastWorkout: time.Time{},
} }
if err := s.userStatsRepo.Create(userStats); err != nil { if err := s.userStatsRepo.Create(userStats); err != nil {
+3 -1
View File
@@ -713,7 +713,9 @@ export default {
} }
.achievement-details { .achievement-details {
space-y: 0.5rem; display: flex;
flex-direction: column;
gap: 1.5rem; /* Современный способ для Flexbox */
} }
.pace { .pace {
+3 -1
View File
@@ -1163,7 +1163,9 @@ export default {
} }
.review-content { .review-content {
space-y: 1rem; display: flex;
flex-direction: column;
gap: 1.5rem; /* Современный способ для Flexbox */
} }
.review-text { .review-text {
+3 -1
View File
@@ -945,7 +945,9 @@ export default {
} }
.signup-form { .signup-form {
space-y: 1.5rem; display: flex;
flex-direction: column;
gap: 1.5rem; /* Современный способ для Flexbox */
} }
.form-group { .form-group {