diff --git a/serv_nginx/api_bb/internal/repository/achievement_repository.go b/serv_nginx/api_bb/internal/repository/achievement_repository.go new file mode 100644 index 0000000..b649148 --- /dev/null +++ b/serv_nginx/api_bb/internal/repository/achievement_repository.go @@ -0,0 +1,244 @@ +// repositories/achievement_repository.go +package repository + +import ( + "time" + + "gorm.io/gorm" + "api_bb/internal/models" +) + +type AchievementRepository interface { + Create(achievement *models.Achievement) error + GetByID(id uint) (*models.Achievement, error) + GetByUserID(userID uint) ([]models.Achievement, error) + GetByUserAndType(userID uint, achievementType models.AchievementType) ([]models.Achievement, error) + GetVerifiedByUserID(userID uint) ([]models.Achievement, error) + GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.Achievement, error) + Update(achievement *models.Achievement) error + Delete(id uint) error + VerifyAchievement(id uint) error + GetUserAchievementsSummary(userID uint) (*models.UserAchievementsResponse, error) + GetRecentAchievements(userID uint, limit int) ([]models.Achievement, error) + CountByType(userID uint) (map[models.AchievementType]int64, error) + ExistsByTitleAndUser(userID uint, title string) (bool, error) +} + +type achievementRepository struct { + db *gorm.DB +} + +func NewAchievementRepository(db *gorm.DB) AchievementRepository { + return &achievementRepository{db: db} +} + +// Create создает новое достижение +func (r *achievementRepository) Create(achievement *models.Achievement) error { + return r.db.Create(achievement).Error +} + +// GetByID возвращает достижение по ID +func (r *achievementRepository) GetByID(id uint) (*models.Achievement, error) { + var achievement models.Achievement + err := r.db.Preload("User").First(&achievement, id).Error + if err != nil { + return nil, err + } + return &achievement, nil +} + +// GetByUserID возвращает все достижения пользователя +func (r *achievementRepository) GetByUserID(userID uint) ([]models.Achievement, error) { + var achievements []models.Achievement + err := r.db.Where("user_id = ?", userID). + Order("date DESC, created_at DESC"). + Find(&achievements).Error + if err != nil { + return nil, err + } + return achievements, nil +} + +// GetByUserAndType возвращает достижения пользователя по типу +func (r *achievementRepository) GetByUserAndType(userID uint, achievementType models.AchievementType) ([]models.Achievement, error) { + var achievements []models.Achievement + err := r.db.Where("user_id = ? AND type = ?", userID, achievementType). + Order("date DESC"). + Find(&achievements).Error + if err != nil { + return nil, err + } + return achievements, nil +} + +// GetVerifiedByUserID возвращает подтвержденные достижения пользователя +func (r *achievementRepository) GetVerifiedByUserID(userID uint) ([]models.Achievement, error) { + var achievements []models.Achievement + err := r.db.Where("user_id = ? AND verified = ?", userID, true). + Order("date DESC"). + Find(&achievements).Error + if err != nil { + return nil, err + } + return achievements, nil +} + +// GetByDateRange возвращает достижения за период времени +func (r *achievementRepository) GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.Achievement, error) { + var achievements []models.Achievement + err := r.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate). + Order("date DESC"). + Find(&achievements).Error + if err != nil { + return nil, err + } + return achievements, nil +} + +// Update обновляет достижение +func (r *achievementRepository) Update(achievement *models.Achievement) error { + return r.db.Save(achievement).Error +} + +// Delete удаляет достижение +func (r *achievementRepository) Delete(id uint) error { + return r.db.Delete(&models.Achievement{}, id).Error +} + +// VerifyAchievement подтверждает достижение +func (r *achievementRepository) VerifyAchievement(id uint) error { + return r.db.Model(&models.Achievement{}). + Where("id = ?", id). + Update("verified", true). + Error +} + +// GetUserAchievementsSummary возвращает сводку по достижениям пользователя +func (r *achievementRepository) GetUserAchievementsSummary(userID uint) (*models.UserAchievementsResponse, error) { + var totalCount int64 + var verifiedCount int64 + + // Считаем общее количество достижений + err := r.db.Model(&models.Achievement{}). + Where("user_id = ?", userID). + Count(&totalCount).Error + if err != nil { + return nil, err + } + + // Считаем количество подтвержденных достижений + err = r.db.Model(&models.Achievement{}). + Where("user_id = ? AND verified = ?", userID, true). + Count(&verifiedCount).Error + if err != nil { + return nil, err + } + + // Получаем все достижения пользователя + achievements, err := r.GetByUserID(userID) + if err != nil { + return nil, err + } + + // Вычисляем процент прогресса + progressPercent := 0.0 + if totalCount > 0 { + progressPercent = (float64(verifiedCount) / float64(totalCount)) * 100 + } + + return &models.UserAchievementsResponse{ + TotalAchievements: int(totalCount), + Completed: int(verifiedCount), + ProgressPercent: progressPercent, + Achievements: achievements, + }, nil +} + +// GetRecentAchievements возвращает последние достижения пользователя +func (r *achievementRepository) GetRecentAchievements(userID uint, limit int) ([]models.Achievement, error) { + var achievements []models.Achievement + err := r.db.Where("user_id = ?", userID). + Order("created_at DESC"). + Limit(limit). + Find(&achievements).Error + if err != nil { + return nil, err + } + return achievements, nil +} + +// CountByType возвращает количество достижений по типам +func (r *achievementRepository) CountByType(userID uint) (map[models.AchievementType]int64, error) { + type CountResult struct { + Type models.AchievementType + Count int64 + } + + var results []CountResult + err := r.db.Model(&models.Achievement{}). + Select("type, COUNT(*) as count"). + Where("user_id = ?", userID). + Group("type"). + Scan(&results).Error + if err != nil { + return nil, err + } + + counts := make(map[models.AchievementType]int64) + for _, result := range results { + counts[result.Type] = result.Count + } + + return counts, nil +} + +// ExistsByTitleAndUser проверяет, существует ли достижение с таким названием у пользователя +func (r *achievementRepository) ExistsByTitleAndUser(userID uint, title string) (bool, error) { + var count int64 + err := r.db.Model(&models.Achievement{}). + Where("user_id = ? AND title = ?", userID, title). + Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +// GetUnverifiedAchievements возвращает неподтвержденные достижения +func (r *achievementRepository) GetUnverifiedAchievements(userID uint) ([]models.Achievement, error) { + var achievements []models.Achievement + err := r.db.Where("user_id = ? AND verified = ?", userID, false). + Order("created_at DESC"). + Find(&achievements).Error + if err != nil { + return nil, err + } + return achievements, nil +} + +// GetAchievementsWithPagination возвращает достижения с пагинацией +func (r *achievementRepository) GetAchievementsWithPagination(userID uint, page, pageSize int) ([]models.Achievement, int64, error) { + var achievements []models.Achievement + var totalCount int64 + + // Считаем общее количество + err := r.db.Model(&models.Achievement{}). + Where("user_id = ?", userID). + Count(&totalCount).Error + if err != nil { + return nil, 0, err + } + + // Получаем данные с пагинацией + offset := (page - 1) * pageSize + err = r.db.Where("user_id = ?", userID). + Order("date DESC, created_at DESC"). + Offset(offset). + Limit(pageSize). + Find(&achievements).Error + if err != nil { + return nil, 0, err + } + + return achievements, totalCount, nil +} \ No newline at end of file diff --git a/serv_nginx/api_bb/internal/repository/personal_best_repository.go b/serv_nginx/api_bb/internal/repository/personal_best_repository.go index 17749f4..e19edd2 100644 --- a/serv_nginx/api_bb/internal/repository/personal_best_repository.go +++ b/serv_nginx/api_bb/internal/repository/personal_best_repository.go @@ -21,6 +21,7 @@ type PersonalBestRepository interface { 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) + CalculatePace(timeStr string, distanceType models.DistanceType) (string, error) } type personalBestRepository struct { diff --git a/serv_nginx/api_bb/internal/service/achievement_service.go b/serv_nginx/api_bb/internal/service/achievement_service.go new file mode 100644 index 0000000..796b872 --- /dev/null +++ b/serv_nginx/api_bb/internal/service/achievement_service.go @@ -0,0 +1,104 @@ +// service/achievement_service.go +package service + +import ( + "api_bb/internal/models" + "api_bb/internal/repository" + "errors" +) + +type AchievementService struct { + achievementRepo repository.AchievementRepository +} + +func NewAchievementService(achievementRepo repository.AchievementRepository) *AchievementService { + return &AchievementService{ + achievementRepo: achievementRepo, + } +} + +// CreateAchievement создает новое достижение +func (s *AchievementService) CreateAchievement(userID uint, req models.AchievementCreateRequest) (*models.Achievement, error) { + // Проверяем, нет ли уже достижения с таким названием у пользователя + exists, err := s.achievementRepo.ExistsByTitleAndUser(userID, req.Title) + if err != nil { + return nil, err + } + if exists { + return nil, ErrAchievementAlreadyExists + } + + achievement := &models.Achievement{ + UserID: userID, + Type: req.Type, + Title: req.Title, + Description: req.Description, + Result: req.Result, + Distance: req.Distance, + Date: req.Date, + BadgeImage: req.BadgeImage, + Verified: false, // По умолчанию не подтверждено + } + + if err := s.achievementRepo.Create(achievement); err != nil { + return nil, err + } + + return achievement, nil +} + +// GetUserAchievements возвращает все достижения пользователя +func (s *AchievementService) GetUserAchievements(userID uint) ([]models.Achievement, error) { + return s.achievementRepo.GetByUserID(userID) +} + +// GetUserAchievementsSummary возвращает сводку по достижениям пользователя +func (s *AchievementService) GetUserAchievementsSummary(userID uint) (*models.UserAchievementsResponse, error) { + return s.achievementRepo.GetUserAchievementsSummary(userID) +} + +// VerifyAchievement подтверждает достижение +func (s *AchievementService) VerifyAchievement(achievementID uint, userID uint) error { + // Проверяем, что достижение принадлежит пользователю + achievement, err := s.achievementRepo.GetByID(achievementID) + if err != nil { + return err + } + + if achievement.UserID != userID { + return ErrAchievementNotFound + } + + return s.achievementRepo.VerifyAchievement(achievementID) +} + +// GetRecentAchievements возвращает последние достижения +func (s *AchievementService) GetRecentAchievements(userID uint, limit int) ([]models.Achievement, error) { + return s.achievementRepo.GetRecentAchievements(userID, limit) +} + +// GetAchievementsByType возвращает достижения по типу +func (s *AchievementService) GetAchievementsByType(userID uint, achievementType models.AchievementType) ([]models.Achievement, error) { + return s.achievementRepo.GetByUserAndType(userID, achievementType) +} + +// DeleteAchievement удаляет достижение +func (s *AchievementService) DeleteAchievement(achievementID uint, userID uint) error { + // Проверяем, что достижение принадлежит пользователю + achievement, err := s.achievementRepo.GetByID(achievementID) + if err != nil { + return err + } + + if achievement.UserID != userID { + return ErrAchievementNotFound + } + + return s.achievementRepo.Delete(achievementID) +} + +// Ошибки +var ( + ErrAchievementAlreadyExists = errors.New("achievement with this title already exists") + ErrAchievementNotFound = errors.New("achievement not found") +) \ No newline at end of file diff --git a/serv_nginx/api_bb/internal/service/personal_best_service.go b/serv_nginx/api_bb/internal/service/personal_best_service.go new file mode 100644 index 0000000..93300ca --- /dev/null +++ b/serv_nginx/api_bb/internal/service/personal_best_service.go @@ -0,0 +1,86 @@ +// services/personal_best_service.go +package service + +import ( + "api_bb/internal/models" + "api_bb/internal/repository" + + "gorm.io/gorm" +) + +type PersonalBestService struct { + pbRepo repository.PersonalBestRepository +} + +func NewPersonalBestService(pbRepo repository.PersonalBestRepository) *PersonalBestService { + return &PersonalBestService{ + pbRepo: pbRepo, + } +} + +// CreatePersonalBest создает новый личный рекорд +func (s *PersonalBestService) CreatePersonalBest(userID uint, req models.PersonalBestCreateRequest) (*models.PersonalBest, error) { + // Вычисляем темп, если не предоставлен + pace := req.Pace + if pace == "" { + calculatedPace, err := s.pbRepo.CalculatePace(req.Time, req.DistanceType) + if err != nil { + return nil, err + } + pace = calculatedPace + } + + // Проверяем, является ли это личным рекордом + isBest, err := s.pbRepo.ExistsBetterTime(userID, req.DistanceType, req.Time) + if err != nil { + return nil, err + } + + personalBest := &models.PersonalBest{ + UserID: userID, + DistanceType: req.DistanceType, + Time: req.Time, + Pace: pace, + Date: req.Date, + EventName: req.EventName, + Location: req.Location, + Verified: false, // По умолчанию не подтвержден + } + + if err := s.pbRepo.Create(personalBest); err != nil { + return nil, err + } + + // Если это лучший результат, можно обновить статистику пользователя + if !isBest { + // Здесь можно вызвать метод для обновления UserStats + } + + return personalBest, nil +} + +// GetUserPersonalBests возвращает все личные рекорды пользователя +func (s *PersonalBestService) GetUserPersonalBests(userID uint) ([]models.PersonalBest, error) { + return s.pbRepo.GetByUserID(userID) +} + +// GetPersonalBestsSummary возвращает сводку лучших результатов +func (s *PersonalBestService) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) { + return s.pbRepo.GetPersonalBestsSummary(userID) +} + +// VerifyPersonalBest подтверждает личный рекорд +func (s *PersonalBestService) VerifyPersonalBest(id uint, userID uint) error { + pb, err := s.pbRepo.GetByID(id) + if err != nil { + return err + } + + // Проверяем, что рекорд принадлежит пользователю + if pb.UserID != userID { + return gorm.ErrRecordNotFound + } + + pb.Verified = true + return s.pbRepo.Update(pb) +} \ No newline at end of file diff --git a/serv_nginx/api_bb/internal/service/user_stats_service.go b/serv_nginx/api_bb/internal/service/user_stats_service.go new file mode 100644 index 0000000..3cd0366 --- /dev/null +++ b/serv_nginx/api_bb/internal/service/user_stats_service.go @@ -0,0 +1,45 @@ +// services/user_stats_service.go +package service + +import ( + "time" + "api_bb/internal/models" + "api_bb/internal/repository" +) + +type UserStatsService struct { + userStatsRepo repository.UserStatsRepository +} + +func NewUserStatsService(userStatsRepo repository.UserStatsRepository) *UserStatsService { + return &UserStatsService{ + userStatsRepo: userStatsRepo, + } +} + +func (s *UserStatsService) AddWorkout(userID uint, distance float64, duration int, workoutTime time.Time) error { + // Обновляем общую статистику + if err := s.userStatsRepo.IncrementWorkouts(userID, distance, duration); err != nil { + return err + } + + // Обновляем серии + if err := s.userStatsRepo.UpdateStreaks(userID, workoutTime); err != nil { + return err + } + + // Обновляем недельный и месячный пробег + if err := s.userStatsRepo.UpdateWeeklyDistance(userID, distance); err != nil { + return err + } + + return s.userStatsRepo.UpdateMonthlyDistance(userID, distance) +} + +func (s *UserStatsService) GetUserStats(userID uint) (*models.UserStatsResponse, error) { + return s.userStatsRepo.GetUserStatsResponse(userID) +} + +func (s *UserStatsService) UpdatePersonalBest(userID uint, distanceType string, time string) error { + return s.userStatsRepo.UpdatePersonalBest(userID, distanceType, time) +} \ No newline at end of file