rename long name to short name
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
// 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
|
||||
}
|
||||
|
||||
// GetVerifiedAchievements возвращает только подтвержденные достижения пользователя
|
||||
func (s *AchievementService) GetVerifiedAchievements(userID uint) ([]models.Achievement, error) {
|
||||
return s.achievementRepo.GetVerifiedByUserID(userID)
|
||||
}
|
||||
|
||||
// GetVerifiedRecentAchievements возвращает последние подтвержденные достижения
|
||||
func (s *AchievementService) GetVerifiedRecentAchievements(userID uint, limit int) ([]models.Achievement, error) {
|
||||
achievements, err := s.achievementRepo.GetRecentAchievements(userID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Фильтруем только подтвержденные
|
||||
var verified []models.Achievement
|
||||
for _, achievement := range achievements {
|
||||
if achievement.Verified {
|
||||
verified = append(verified, achievement)
|
||||
}
|
||||
}
|
||||
|
||||
return verified, 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)
|
||||
}
|
||||
|
||||
// GetAchievementByID возвращает достижение по ID
|
||||
func (s *AchievementService) GetAchievementByID(achievementID uint, userID uint) (*models.Achievement, error) {
|
||||
achievement, err := s.achievementRepo.GetByID(achievementID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, что достижение принадлежит пользователю
|
||||
if achievement.UserID != userID {
|
||||
return nil, ErrAchievementNotFound
|
||||
}
|
||||
|
||||
return achievement, nil
|
||||
}
|
||||
|
||||
// UpdateAchievement обновляет достижение
|
||||
func (s *AchievementService) UpdateAchievement(achievementID uint, userID uint, req models.AchievementCreateRequest) (*models.Achievement, error) {
|
||||
// Проверяем, что достижение принадлежит пользователю
|
||||
existingAchievement, err := s.achievementRepo.GetByID(achievementID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if existingAchievement.UserID != userID {
|
||||
return nil, ErrAchievementNotFound
|
||||
}
|
||||
|
||||
// Проверяем, нет ли другого достижения с таким названием
|
||||
if existingAchievement.Title != req.Title {
|
||||
exists, err := s.achievementRepo.ExistsByTitleAndUser(userID, req.Title)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exists {
|
||||
return nil, ErrAchievementAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем данные
|
||||
existingAchievement.Type = req.Type
|
||||
existingAchievement.Title = req.Title
|
||||
existingAchievement.Description = req.Description
|
||||
existingAchievement.Result = req.Result
|
||||
existingAchievement.Distance = req.Distance
|
||||
existingAchievement.Date = req.Date
|
||||
existingAchievement.BadgeImage = req.BadgeImage
|
||||
|
||||
if err := s.achievementRepo.Update(existingAchievement); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return existingAchievement, nil
|
||||
}
|
||||
|
||||
// Ошибки
|
||||
var (
|
||||
ErrAchievementAlreadyExists = errors.New("achievement with this title already exists")
|
||||
ErrAchievementNotFound = errors.New("achievement not found")
|
||||
)
|
||||
@@ -0,0 +1,122 @@
|
||||
// service/auth_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type AuthService interface {
|
||||
Register(user *models.User) error
|
||||
Login(email, password string) (*models.User, string, error)
|
||||
}
|
||||
|
||||
type authService struct {
|
||||
userRepo repository.UserRepository
|
||||
jwtService JWTService
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewAuthService(userRepo repository.UserRepository, jwtService JWTService, log logger.LoggerInterface) AuthService {
|
||||
// Создаем логгер с контекстом для сервиса
|
||||
serviceLogger := log.With(zap.String("service", "auth"))
|
||||
|
||||
return &authService{
|
||||
userRepo: userRepo,
|
||||
jwtService: jwtService,
|
||||
logger: serviceLogger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *authService) Register(user *models.User) error {
|
||||
s.logger.Info("Registering new user",
|
||||
zap.String("email", user.Email),
|
||||
)
|
||||
|
||||
existingUser, err := s.userRepo.FindByEmail(user.Email)
|
||||
if err == nil && existingUser != nil {
|
||||
s.logger.Warn("Registration failed - email already exists",
|
||||
zap.String("email", user.Email),
|
||||
)
|
||||
return errors.New("user with this email already exists")
|
||||
}
|
||||
|
||||
err = s.userRepo.Create(user)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create user in database",
|
||||
zap.String("email", user.Email),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
s.logger.Info("User registered successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", user.Email),
|
||||
)
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *authService) Login(email, password string) (*models.User, string, error) {
|
||||
s.logger.Info("Login attempt",
|
||||
zap.String("email", email),
|
||||
zap.Int("password_length", len(password)),
|
||||
)
|
||||
|
||||
user, err := s.userRepo.FindByEmail(email)
|
||||
if err != nil {
|
||||
s.logger.Warn("Login failed - user not found",
|
||||
zap.String("email", email),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, "", errors.New("invalid email")
|
||||
}
|
||||
|
||||
s.logger.Debug("User found for login",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("stored_hash_prefix", user.Password[:min(10, len(user.Password))]),
|
||||
)
|
||||
|
||||
// Проверяем пароль
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
|
||||
if err != nil {
|
||||
s.logger.Warn("Login failed - invalid password",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", email),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, "", errors.New("invalid password")
|
||||
}
|
||||
|
||||
s.logger.Info("Login successful",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", email),
|
||||
)
|
||||
|
||||
token, err := s.jwtService.GenerateToken(user.ID, user.Email)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate JWT token",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
return user, token, nil
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
// service/avatar_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AvatarService interface {
|
||||
UploadAvatar(userID uint, file multipart.File, header *multipart.FileHeader) (string, error)
|
||||
DeleteAvatar(userID uint) error
|
||||
GetAvatarPath(userID uint) (string, error)
|
||||
GetAvatarFile(filename string) ([]byte, string, error)
|
||||
ServeAvatarFile(w io.Writer, filename string) (string, error)
|
||||
}
|
||||
|
||||
type avatarService struct {
|
||||
userRepo repository.UserRepository
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewAvatarService(userRepo repository.UserRepository, log logger.LoggerInterface) AvatarService {
|
||||
return &avatarService{
|
||||
userRepo: userRepo,
|
||||
logger: log.With(zap.String("service", "avatar")),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *avatarService) UploadAvatar(userID uint, file multipart.File, header *multipart.FileHeader) (string, error) {
|
||||
// Проверяем пользователя
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// Создаем директорию для аватаров если не существует
|
||||
uploadDir := "./uploads/avatars"
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create upload directory: %v", err)
|
||||
}
|
||||
|
||||
// Генерируем уникальное имя файла
|
||||
fileExt := filepath.Ext(header.Filename)
|
||||
fileName := fmt.Sprintf("avatar_%d_%d%s", userID, time.Now().Unix(), fileExt)
|
||||
filePath := filepath.Join(uploadDir, fileName)
|
||||
|
||||
// Создаем файл
|
||||
dst, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create file: %v", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// Копируем содержимое
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
return "", fmt.Errorf("failed to save file: %v", err)
|
||||
}
|
||||
|
||||
// Удаляем старый аватар если существует
|
||||
if user.Avatar != "" {
|
||||
oldPath := strings.TrimPrefix(user.Avatar, "/")
|
||||
if _, err := os.Stat(oldPath); err == nil {
|
||||
os.Remove(oldPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем путь в БД
|
||||
avatarPath := "/uploads/avatars/" + fileName
|
||||
if err := s.userRepo.UpdateAvatar(userID, avatarPath); err != nil {
|
||||
// Если не удалось сохранить в БД, удаляем загруженный файл
|
||||
os.Remove(filePath)
|
||||
return "", fmt.Errorf("failed to update avatar in database: %v", err)
|
||||
}
|
||||
|
||||
return avatarPath, nil
|
||||
}
|
||||
|
||||
func (s *avatarService) DeleteAvatar(userID uint) error {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
if user.Avatar == "" {
|
||||
return nil // Аватара нет, ничего не делаем
|
||||
}
|
||||
|
||||
// Удаляем файл
|
||||
filePath := strings.TrimPrefix(user.Avatar, "/")
|
||||
if _, err := os.Stat(filePath); err == nil {
|
||||
if err := os.Remove(filePath); err != nil {
|
||||
s.logger.Warn("Failed to delete avatar file", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем поле в БД
|
||||
return s.userRepo.UpdateAvatar(userID, "")
|
||||
}
|
||||
|
||||
func (s *avatarService) GetAvatarPath(userID uint) (string, error) {
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return user.Avatar, nil
|
||||
}
|
||||
|
||||
func (s *avatarService) GetAvatarFile(filename string) ([]byte, string, error) {
|
||||
// Валидация имени файла
|
||||
if filename == "" || strings.Contains(filename, "..") || strings.Contains(filename, "/") {
|
||||
return nil, "", fmt.Errorf("invalid filename")
|
||||
}
|
||||
|
||||
// Проверяем допустимые расширения
|
||||
allowedExts := map[string]string{
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
}
|
||||
|
||||
fileExt := strings.ToLower(filepath.Ext(filename))
|
||||
contentType, exists := allowedExts[fileExt]
|
||||
if !exists {
|
||||
return nil, "", fmt.Errorf("unsupported file format")
|
||||
}
|
||||
|
||||
// Формируем путь к файлу
|
||||
filePath := filepath.Join("./uploads/avatars", filename)
|
||||
|
||||
// Проверяем существование файла
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, "", fmt.Errorf("avatar file not found")
|
||||
}
|
||||
return nil, "", fmt.Errorf("failed to access file: %v", err)
|
||||
}
|
||||
|
||||
// Проверяем размер файла (максимум 10MB)
|
||||
if fileInfo.Size() > 10*1024*1024 {
|
||||
return nil, "", fmt.Errorf("file too large")
|
||||
}
|
||||
|
||||
// Читаем файл
|
||||
fileData, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to read file: %v", err)
|
||||
}
|
||||
|
||||
return fileData, contentType, nil
|
||||
}
|
||||
|
||||
func (s *avatarService) ServeAvatarFile(w io.Writer, filename string) (string, error) {
|
||||
// Валидация имени файла
|
||||
if filename == "" || strings.Contains(filename, "..") || strings.Contains(filename, "/") {
|
||||
return "", fmt.Errorf("invalid filename")
|
||||
}
|
||||
|
||||
// Проверяем допустимые расширения
|
||||
allowedExts := map[string]string{
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
}
|
||||
|
||||
fileExt := strings.ToLower(filepath.Ext(filename))
|
||||
contentType, exists := allowedExts[fileExt]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("unsupported file format")
|
||||
}
|
||||
|
||||
// Формируем путь к файлу
|
||||
filePath := filepath.Join("./uploads/avatars", filename)
|
||||
|
||||
// Проверяем существование файла
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", fmt.Errorf("avatar file not found")
|
||||
}
|
||||
return "", fmt.Errorf("failed to access file: %v", err)
|
||||
}
|
||||
|
||||
// Проверяем размер файла
|
||||
if fileInfo.Size() > 10*1024*1024 {
|
||||
return "", fmt.Errorf("file too large")
|
||||
}
|
||||
|
||||
// Открываем и копируем файл
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(w, file)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to serve file: %v", err)
|
||||
}
|
||||
|
||||
return contentType, nil
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
// service/email_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/email"
|
||||
"api_bb/pkg/logger"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type EmailService struct {
|
||||
emailRepo repository.EmailRepository
|
||||
userRepo repository.UserRepository
|
||||
emailSender email.Service
|
||||
logger *zap.Logger
|
||||
tokenExpiry time.Duration
|
||||
passwordExpiry time.Duration
|
||||
}
|
||||
|
||||
func NewEmailService(
|
||||
emailRepo repository.EmailRepository,
|
||||
userRepo repository.UserRepository,
|
||||
emailSender email.Service,
|
||||
) EmailService {
|
||||
// Создаем логгер с контекстом для сервиса
|
||||
serviceLogger := logger.Get().With(zap.String("service", "email"))
|
||||
|
||||
return EmailService{
|
||||
emailRepo: emailRepo,
|
||||
userRepo: userRepo,
|
||||
emailSender: emailSender,
|
||||
logger: serviceLogger,
|
||||
tokenExpiry: 24 * time.Hour, // 24 часа для верификации
|
||||
passwordExpiry: 1 * time.Hour, // 1 час для сброса пароля
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EmailService) SendVerificationEmail(userID uint, email, userName string) error {
|
||||
s.logger.Info("Sending verification email",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("email", email),
|
||||
)
|
||||
|
||||
token := uuid.New().String()
|
||||
|
||||
verification := &models.EmailVerification{
|
||||
UserID: userID,
|
||||
Token: token,
|
||||
Email: email,
|
||||
Type: "verification",
|
||||
ExpiresAt: time.Now().Add(s.tokenExpiry),
|
||||
}
|
||||
|
||||
if err := s.emailRepo.CreateVerificationToken(verification); err != nil {
|
||||
s.logger.Error("Failed to create verification token",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("email", email),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to create verification token: %w", err)
|
||||
}
|
||||
|
||||
if err := s.emailSender.SendVerificationEmail(email, userName, token); err != nil {
|
||||
s.logger.Error("Failed to send verification email",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("email", email),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to send verification email: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Verification email sent successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("email", email))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EmailService) VerifyEmail(token string) error {
|
||||
s.logger.Info("Verifying email token",
|
||||
zap.String("token", token),
|
||||
)
|
||||
|
||||
verification, err := s.emailRepo.GetVerificationToken(token)
|
||||
if err != nil {
|
||||
s.logger.Error("Invalid or expired verification token",
|
||||
zap.String("token", token),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("invalid or expired token: %w", err)
|
||||
}
|
||||
|
||||
if verification.Type != "verification" {
|
||||
s.logger.Error("Invalid token type for email verification",
|
||||
zap.String("token", token),
|
||||
zap.String("type", verification.Type),
|
||||
)
|
||||
return fmt.Errorf("invalid token type")
|
||||
}
|
||||
|
||||
// Обновляем пользователя
|
||||
if err := s.userRepo.MarkEmailAsVerified(verification.UserID); err != nil {
|
||||
s.logger.Error("Failed to verify email in user repository",
|
||||
zap.Uint("user_id", verification.UserID),
|
||||
zap.String("email", verification.Email),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to verify email: %w", err)
|
||||
}
|
||||
|
||||
// Помечаем токен как использованный
|
||||
if err := s.emailRepo.MarkTokenAsUsed(token); err != nil {
|
||||
s.logger.Error("Failed to mark token as used",
|
||||
zap.Error(err),
|
||||
zap.String("token", token))
|
||||
}
|
||||
|
||||
s.logger.Info("Email verified successfully",
|
||||
zap.Uint("user_id", verification.UserID),
|
||||
zap.String("email", verification.Email))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EmailService) SendPasswordResetEmail(email string) error {
|
||||
s.logger.Info("Sending password reset email",
|
||||
zap.String("email", email),
|
||||
)
|
||||
|
||||
user, err := s.userRepo.FindByEmail(email)
|
||||
if err != nil {
|
||||
// Для безопасности не сообщаем, существует ли email
|
||||
s.logger.Info("Password reset requested for non-existent email",
|
||||
zap.String("email", email))
|
||||
return nil
|
||||
}
|
||||
|
||||
token := uuid.New().String()
|
||||
|
||||
resetRequest := &models.EmailVerification{
|
||||
UserID: user.ID,
|
||||
Token: token,
|
||||
Email: email,
|
||||
Type: "password_reset",
|
||||
ExpiresAt: time.Now().Add(s.passwordExpiry),
|
||||
}
|
||||
|
||||
if err := s.emailRepo.CreateVerificationToken(resetRequest); err != nil {
|
||||
s.logger.Error("Failed to create password reset token",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", email),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to create password reset token: %w", err)
|
||||
}
|
||||
|
||||
if err := s.emailSender.SendPasswordResetEmail(email, user.FirstName, token); err != nil {
|
||||
s.logger.Error("Failed to send password reset email",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", email),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to send password reset email: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Password reset email sent successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", email))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EmailService) ResetPassword(token, newPassword string) error {
|
||||
s.logger.Info("Resetting password with token",
|
||||
zap.String("token", token),
|
||||
)
|
||||
|
||||
verification, err := s.emailRepo.GetVerificationToken(token)
|
||||
if err != nil {
|
||||
s.logger.Error("Invalid or expired password reset token",
|
||||
zap.String("token", token),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("invalid or expired token: %w", err)
|
||||
}
|
||||
|
||||
if verification.Type != "password_reset" {
|
||||
s.logger.Error("Invalid token type for password reset",
|
||||
zap.String("token", token),
|
||||
zap.String("type", verification.Type),
|
||||
)
|
||||
return fmt.Errorf("invalid token type")
|
||||
}
|
||||
|
||||
// Обновляем пароль пользователя
|
||||
if err := s.userRepo.UpdatePassword(verification.UserID, newPassword); err != nil {
|
||||
s.logger.Error("Failed to update password",
|
||||
zap.Uint("user_id", verification.UserID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
}
|
||||
|
||||
// Помечаем токен как использованный
|
||||
if err := s.emailRepo.MarkTokenAsUsed(token); err != nil {
|
||||
s.logger.Error("Failed to mark token as used",
|
||||
zap.Error(err),
|
||||
zap.String("token", token))
|
||||
}
|
||||
|
||||
s.logger.Info("Password reset successfully",
|
||||
zap.Uint("user_id", verification.UserID))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EmailService) SendNewsletterToSubscribers(subject, content string) error {
|
||||
s.logger.Info("Sending newsletter to subscribers",
|
||||
zap.String("subject", subject),
|
||||
)
|
||||
|
||||
subscribers, err := s.emailRepo.GetUsersWithNewsletter()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get subscribers",
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to get subscribers: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Found subscribers for newsletter",
|
||||
zap.Int("count", len(subscribers)),
|
||||
)
|
||||
|
||||
var errors []error
|
||||
for _, user := range subscribers {
|
||||
if err := s.emailSender.SendNewsletterEmail(user.Email, user.FirstName, subject, content); err != nil {
|
||||
s.logger.Error("Failed to send newsletter to user",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", user.Email),
|
||||
zap.Error(err))
|
||||
errors = append(errors, err)
|
||||
continue
|
||||
}
|
||||
s.logger.Debug("Newsletter sent to user",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", user.Email))
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
s.logger.Error("Failed to send newsletter to some users",
|
||||
zap.Int("failed_count", len(errors)),
|
||||
zap.Int("total_subscribers", len(subscribers)),
|
||||
)
|
||||
return fmt.Errorf("failed to send newsletter to %d users", len(errors))
|
||||
}
|
||||
|
||||
s.logger.Info("Newsletter sent to all subscribers",
|
||||
zap.Int("total_subscribers", len(subscribers)))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *EmailService) CleanupExpiredTokens() error {
|
||||
s.logger.Info("Cleaning up expired tokens")
|
||||
|
||||
if err := s.emailRepo.DeleteExpiredTokens(); err != nil {
|
||||
s.logger.Error("Failed to cleanup expired tokens",
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to cleanup expired tokens: %w", err)
|
||||
}
|
||||
s.logger.Info("Expired tokens cleaned up successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserByID возвращает пользователя по ID
|
||||
func (s *EmailService) GetUserByID(userID uint) (*models.User, error) {
|
||||
s.logger.Info("Getting user by ID",
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
user, err := s.userRepo.GetUserByID(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get user by ID",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("User retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("email", user.Email),
|
||||
)
|
||||
return user, nil
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
// service/event_registration_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type EventRegistrationService interface {
|
||||
RegisterForEvent(registration *models.EventRegistration) error
|
||||
GetRegistrationByID(id uint) (*models.EventRegistration, error)
|
||||
GetRegistrationsByEventID(eventID uint) ([]models.EventRegistration, error)
|
||||
GetRegistrationsByUserID(userID uint) ([]models.EventRegistration, error)
|
||||
GetRegistrationByEventAndUser(eventID, userID uint) (*models.EventRegistration, error)
|
||||
UpdateRegistration(registration *models.EventRegistration) error
|
||||
CancelRegistration(id uint) error
|
||||
UpdateRegistrationStatus(registrationID uint, status string) error
|
||||
UpdateResultTime(registrationID uint, resultTime string) error
|
||||
CheckEventAvailability(eventID uint) (bool, error)
|
||||
}
|
||||
|
||||
type eventRegistrationService struct {
|
||||
registrationRepo repository.EventRegistrationRepository
|
||||
eventRepo repository.EventRepository
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewEventRegistrationService(
|
||||
registrationRepo repository.EventRegistrationRepository,
|
||||
eventRepo repository.EventRepository,
|
||||
log logger.LoggerInterface,
|
||||
) EventRegistrationService {
|
||||
serviceLogger := log.With(zap.String("service", "event_registration"))
|
||||
|
||||
return &eventRegistrationService{
|
||||
registrationRepo: registrationRepo,
|
||||
eventRepo: eventRepo,
|
||||
logger: serviceLogger,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterForEvent регистрирует пользователя на событие
|
||||
func (s *eventRegistrationService) RegisterForEvent(registration *models.EventRegistration) error {
|
||||
s.logger.Info("Registering user for event",
|
||||
zap.Uint("user_id", registration.UserID),
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
)
|
||||
|
||||
// Проверяем существование события
|
||||
event, err := s.eventRepo.FindByID(registration.EventID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Event not found for registration",
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("event not found")
|
||||
}
|
||||
|
||||
// Проверяем, открыта ли регистрация
|
||||
if !event.RegistrationOpen {
|
||||
s.logger.Warn("Registration is closed for event",
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
zap.String("event_title", event.Title),
|
||||
)
|
||||
return fmt.Errorf("registration is closed for this event")
|
||||
}
|
||||
|
||||
// Проверяем, не зарегистрирован ли пользователь уже
|
||||
existingRegistration, err := s.registrationRepo.FindByEventAndUser(registration.EventID, registration.UserID)
|
||||
if err == nil && existingRegistration != nil {
|
||||
s.logger.Warn("User already registered for event",
|
||||
zap.Uint("user_id", registration.UserID),
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
)
|
||||
return fmt.Errorf("user already registered for this event")
|
||||
}
|
||||
|
||||
// Проверяем доступность мест
|
||||
available, err := s.CheckEventAvailability(registration.EventID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to check event availability",
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to check event availability: %w", err)
|
||||
}
|
||||
|
||||
if !available {
|
||||
s.logger.Warn("Event is full",
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
zap.String("event_title", event.Title),
|
||||
)
|
||||
return fmt.Errorf("event is full")
|
||||
}
|
||||
|
||||
// Создаем регистрацию
|
||||
if err := s.registrationRepo.Create(registration); err != nil {
|
||||
s.logger.Error("Failed to create registration",
|
||||
zap.Uint("user_id", registration.UserID),
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to register for event: %w", err)
|
||||
}
|
||||
|
||||
// Обновляем счетчик участников
|
||||
if err := s.eventRepo.UpdateParticipantsCount(registration.EventID, event.ParticipantsCount+1); err != nil {
|
||||
s.logger.Error("Failed to update participants count",
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
zap.Error(err),
|
||||
)
|
||||
// Не прерываем выполнение, только логируем ошибку
|
||||
}
|
||||
|
||||
s.logger.Info("User registered for event successfully",
|
||||
zap.Uint("user_id", registration.UserID),
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
zap.String("status", registration.Status),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRegistrationByID возвращает регистрацию по ID
|
||||
func (s *eventRegistrationService) GetRegistrationByID(id uint) (*models.EventRegistration, error) {
|
||||
s.logger.Debug("Getting registration by ID", zap.Uint("registration_id", id))
|
||||
|
||||
registration, err := s.registrationRepo.FindByID(id)
|
||||
if err != nil {
|
||||
s.logger.Warn("Registration not found",
|
||||
zap.Uint("registration_id", id),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("registration not found: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Registration retrieved successfully",
|
||||
zap.Uint("registration_id", id),
|
||||
zap.Uint("user_id", registration.UserID),
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
)
|
||||
return registration, nil
|
||||
}
|
||||
|
||||
// GetRegistrationsByEventID возвращает все регистрации на событие
|
||||
func (s *eventRegistrationService) GetRegistrationsByEventID(eventID uint) ([]models.EventRegistration, error) {
|
||||
s.logger.Debug("Getting registrations by event ID", zap.Uint("event_id", eventID))
|
||||
|
||||
registrations, err := s.registrationRepo.FindByEventID(eventID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get registrations by event ID",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("failed to get registrations: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Registrations by event retrieved successfully",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Int("count", len(registrations)),
|
||||
)
|
||||
return registrations, nil
|
||||
}
|
||||
|
||||
// GetRegistrationsByUserID возвращает все регистрации пользователя
|
||||
func (s *eventRegistrationService) GetRegistrationsByUserID(userID uint) ([]models.EventRegistration, error) {
|
||||
s.logger.Debug("Getting registrations by user ID", zap.Uint("user_id", userID))
|
||||
|
||||
registrations, err := s.registrationRepo.FindByUserID(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get registrations by user ID",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("failed to get user registrations: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("User registrations retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("count", len(registrations)),
|
||||
)
|
||||
return registrations, nil
|
||||
}
|
||||
|
||||
// GetRegistrationByEventAndUser возвращает регистрацию по событию и пользователю
|
||||
func (s *eventRegistrationService) GetRegistrationByEventAndUser(eventID, userID uint) (*models.EventRegistration, error) {
|
||||
s.logger.Debug("Getting registration by event and user",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
registration, err := s.registrationRepo.FindByEventAndUser(eventID, userID)
|
||||
if err != nil {
|
||||
s.logger.Debug("Registration not found for event and user",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("registration not found: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Registration by event and user retrieved successfully",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
return registration, nil
|
||||
}
|
||||
|
||||
// UpdateRegistration обновляет регистрацию
|
||||
func (s *eventRegistrationService) UpdateRegistration(registration *models.EventRegistration) error {
|
||||
s.logger.Info("Updating registration",
|
||||
zap.Uint("registration_id", registration.ID),
|
||||
zap.Uint("user_id", registration.UserID),
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
)
|
||||
|
||||
// Проверяем существование регистрации
|
||||
existingRegistration, err := s.registrationRepo.FindByID(registration.ID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Registration not found for update",
|
||||
zap.Uint("registration_id", registration.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("registration not found")
|
||||
}
|
||||
|
||||
// Сохраняем неизменяемые поля
|
||||
registration.CreatedAt = existingRegistration.CreatedAt
|
||||
|
||||
if err := s.registrationRepo.Update(registration); err != nil {
|
||||
s.logger.Error("Failed to update registration",
|
||||
zap.Uint("registration_id", registration.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to update registration: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Registration updated successfully",
|
||||
zap.Uint("registration_id", registration.ID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelRegistration отменяет регистрацию
|
||||
func (s *eventRegistrationService) CancelRegistration(id uint) error {
|
||||
s.logger.Info("Canceling registration", zap.Uint("registration_id", id))
|
||||
|
||||
// Получаем регистрацию для получения event_id
|
||||
registration, err := s.registrationRepo.FindByID(id)
|
||||
if err != nil {
|
||||
s.logger.Warn("Registration not found for cancellation",
|
||||
zap.Uint("registration_id", id),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("registration not found")
|
||||
}
|
||||
|
||||
if err := s.registrationRepo.Delete(id); err != nil {
|
||||
s.logger.Error("Failed to cancel registration",
|
||||
zap.Uint("registration_id", id),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to cancel registration: %w", err)
|
||||
}
|
||||
|
||||
// Обновляем счетчик участников
|
||||
if err := s.eventRepo.UpdateParticipantsCount(registration.EventID, registration.Event.ParticipantsCount-1); err != nil {
|
||||
s.logger.Error("Failed to update participants count after cancellation",
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
zap.Error(err),
|
||||
)
|
||||
// Не прерываем выполнение, только логируем ошибку
|
||||
}
|
||||
|
||||
s.logger.Info("Registration canceled successfully",
|
||||
zap.Uint("registration_id", id),
|
||||
zap.Uint("event_id", registration.EventID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRegistrationStatus обновляет статус регистрации
|
||||
func (s *eventRegistrationService) UpdateRegistrationStatus(registrationID uint, status string) error {
|
||||
s.logger.Info("Updating registration status",
|
||||
zap.Uint("registration_id", registrationID),
|
||||
zap.String("status", status),
|
||||
)
|
||||
|
||||
validStatuses := []string{"pending", "confirmed", "cancelled", "completed"}
|
||||
if !contains(validStatuses, status) {
|
||||
s.logger.Warn("Invalid registration status",
|
||||
zap.String("status", status),
|
||||
zap.Strings("valid_statuses", validStatuses),
|
||||
)
|
||||
return fmt.Errorf("invalid status: %s", status)
|
||||
}
|
||||
|
||||
if err := s.registrationRepo.UpdateStatus(registrationID, status); err != nil {
|
||||
s.logger.Error("Failed to update registration status",
|
||||
zap.Uint("registration_id", registrationID),
|
||||
zap.String("status", status),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to update registration status: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Registration status updated successfully",
|
||||
zap.Uint("registration_id", registrationID),
|
||||
zap.String("status", status),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateResultTime обновляет результат забега
|
||||
func (s *eventRegistrationService) UpdateResultTime(registrationID uint, resultTime string) error {
|
||||
s.logger.Info("Updating result time",
|
||||
zap.Uint("registration_id", registrationID),
|
||||
zap.String("result_time", resultTime),
|
||||
)
|
||||
|
||||
if err := s.registrationRepo.UpdateResultTime(registrationID, resultTime); err != nil {
|
||||
s.logger.Error("Failed to update result time",
|
||||
zap.Uint("registration_id", registrationID),
|
||||
zap.String("result_time", resultTime),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to update result time: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Result time updated successfully",
|
||||
zap.Uint("registration_id", registrationID),
|
||||
zap.String("result_time", resultTime),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckEventAvailability проверяет доступность мест на событии
|
||||
func (s *eventRegistrationService) CheckEventAvailability(eventID uint) (bool, error) {
|
||||
s.logger.Debug("Checking event availability", zap.Uint("event_id", eventID))
|
||||
|
||||
event, err := s.eventRepo.FindByID(eventID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("event not found: %w", err)
|
||||
}
|
||||
|
||||
// Если максимальное количество участников не установлено, считаем доступным
|
||||
if event.MaxParticipants == 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Получаем текущее количество подтвержденных регистраций
|
||||
currentCount, err := s.registrationRepo.CountByEventID(eventID)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to count registrations: %w", err)
|
||||
}
|
||||
|
||||
available := int(currentCount) < event.MaxParticipants
|
||||
|
||||
s.logger.Debug("Event availability check completed",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Int64("current_count", currentCount),
|
||||
zap.Int("max_participants", event.MaxParticipants),
|
||||
zap.Bool("available", available),
|
||||
)
|
||||
|
||||
return available, nil
|
||||
}
|
||||
|
||||
// contains проверяет наличие строки в слайсе
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// service/event_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type EventService interface {
|
||||
CreateEvent(event *models.Event) error
|
||||
GetEventByID(id uint) (*models.Event, error)
|
||||
GetAllEvents() ([]models.Event, error)
|
||||
UpdateEvent(event *models.Event) error
|
||||
DeleteEvent(id uint) error
|
||||
GetEventsByType(eventType models.EventType) ([]models.Event, error)
|
||||
GetUpcomingEvents() ([]models.Event, error)
|
||||
GetEventsByDateRange(startDate, endDate time.Time) ([]models.Event, error)
|
||||
UpdateParticipantsCount(eventID uint) error
|
||||
ToggleRegistrationStatus(eventID uint, registrationOpen bool) error
|
||||
}
|
||||
|
||||
type eventService struct {
|
||||
eventRepo repository.EventRepository
|
||||
registrationRepo repository.EventRegistrationRepository
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewEventService(
|
||||
eventRepo repository.EventRepository,
|
||||
registrationRepo repository.EventRegistrationRepository,
|
||||
log logger.LoggerInterface,
|
||||
) EventService {
|
||||
serviceLogger := log.With(zap.String("service", "event"))
|
||||
|
||||
return &eventService{
|
||||
eventRepo: eventRepo,
|
||||
registrationRepo: registrationRepo,
|
||||
logger: serviceLogger,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateEvent создает новое событие
|
||||
func (s *eventService) CreateEvent(event *models.Event) error {
|
||||
s.logger.Info("Creating new event",
|
||||
zap.String("title", event.Title),
|
||||
zap.String("type", string(event.Type)),
|
||||
zap.Time("date", event.Date),
|
||||
)
|
||||
|
||||
if err := s.eventRepo.Create(event); err != nil {
|
||||
s.logger.Error("Failed to create event",
|
||||
zap.String("title", event.Title),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to create event: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Event created successfully",
|
||||
zap.Uint("event_id", event.ID),
|
||||
zap.String("title", event.Title),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEventByID возвращает событие по ID
|
||||
func (s *eventService) GetEventByID(id uint) (*models.Event, error) {
|
||||
s.logger.Debug("Getting event by ID", zap.Uint("event_id", id))
|
||||
|
||||
event, err := s.eventRepo.FindByID(id)
|
||||
if err != nil {
|
||||
s.logger.Warn("Event not found",
|
||||
zap.Uint("event_id", id),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("event not found: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Event retrieved successfully",
|
||||
zap.Uint("event_id", id),
|
||||
zap.String("title", event.Title),
|
||||
)
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// GetAllEvents возвращает все события
|
||||
func (s *eventService) GetAllEvents() ([]models.Event, error) {
|
||||
s.logger.Debug("Getting all events")
|
||||
|
||||
events, err := s.eventRepo.FindAll()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get events", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to get events: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Events retrieved successfully",
|
||||
zap.Int("count", len(events)),
|
||||
)
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// UpdateEvent обновляет событие
|
||||
func (s *eventService) UpdateEvent(event *models.Event) error {
|
||||
s.logger.Info("Updating event",
|
||||
zap.Uint("event_id", event.ID),
|
||||
zap.String("title", event.Title),
|
||||
)
|
||||
|
||||
// Проверяем существование события
|
||||
existingEvent, err := s.eventRepo.FindByID(event.ID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Event not found for update",
|
||||
zap.Uint("event_id", event.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("event not found")
|
||||
}
|
||||
|
||||
// Сохраняем неизменяемые поля
|
||||
event.CreatedAt = existingEvent.CreatedAt
|
||||
event.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.eventRepo.Update(event); err != nil {
|
||||
s.logger.Error("Failed to update event",
|
||||
zap.Uint("event_id", event.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to update event: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Event updated successfully",
|
||||
zap.Uint("event_id", event.ID),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteEvent удаляет событие
|
||||
func (s *eventService) DeleteEvent(id uint) error {
|
||||
s.logger.Info("Deleting event", zap.Uint("event_id", id))
|
||||
|
||||
// Проверяем существование события
|
||||
_, err := s.eventRepo.FindByID(id)
|
||||
if err != nil {
|
||||
s.logger.Warn("Event not found for deletion",
|
||||
zap.Uint("event_id", id),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("event not found")
|
||||
}
|
||||
|
||||
if err := s.eventRepo.Delete(id); err != nil {
|
||||
s.logger.Error("Failed to delete event",
|
||||
zap.Uint("event_id", id),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to delete event: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Event deleted successfully",
|
||||
zap.Uint("event_id", id),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEventsByType возвращает события по типу
|
||||
func (s *eventService) GetEventsByType(eventType models.EventType) ([]models.Event, error) {
|
||||
s.logger.Debug("Getting events by type", zap.String("type", string(eventType)))
|
||||
|
||||
events, err := s.eventRepo.FindByType(eventType)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get events by type",
|
||||
zap.String("type", string(eventType)),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("failed to get events by type: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Events by type retrieved successfully",
|
||||
zap.String("type", string(eventType)),
|
||||
zap.Int("count", len(events)),
|
||||
)
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// GetUpcomingEvents возвращает предстоящие события
|
||||
func (s *eventService) GetUpcomingEvents() ([]models.Event, error) {
|
||||
s.logger.Debug("Getting upcoming events")
|
||||
|
||||
events, err := s.eventRepo.FindUpcoming()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get upcoming events", zap.Error(err))
|
||||
return nil, fmt.Errorf("failed to get upcoming events: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Upcoming events retrieved successfully",
|
||||
zap.Int("count", len(events)),
|
||||
)
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// GetEventsByDateRange возвращает события в диапазоне дат
|
||||
func (s *eventService) GetEventsByDateRange(startDate, endDate time.Time) ([]models.Event, error) {
|
||||
s.logger.Debug("Getting events by date range",
|
||||
zap.Time("start_date", startDate),
|
||||
zap.Time("end_date", endDate),
|
||||
)
|
||||
|
||||
events, err := s.eventRepo.FindByDateRange(startDate, endDate)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get events by date range",
|
||||
zap.Time("start_date", startDate),
|
||||
zap.Time("end_date", endDate),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("failed to get events by date range: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Events by date range retrieved successfully",
|
||||
zap.Time("start_date", startDate),
|
||||
zap.Time("end_date", endDate),
|
||||
zap.Int("count", len(events)),
|
||||
)
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// UpdateParticipantsCount обновляет количество участников события
|
||||
func (s *eventService) UpdateParticipantsCount(eventID uint) error {
|
||||
s.logger.Debug("Updating participants count", zap.Uint("event_id", eventID))
|
||||
|
||||
count, err := s.registrationRepo.CountByEventID(eventID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to count event registrations",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to count registrations: %w", err)
|
||||
}
|
||||
|
||||
if err := s.eventRepo.UpdateParticipantsCount(eventID, int(count)); err != nil {
|
||||
s.logger.Error("Failed to update participants count",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Int64("count", count),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to update participants count: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Participants count updated successfully",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Int64("count", count),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToggleRegistrationStatus переключает статус регистрации на событие
|
||||
func (s *eventService) ToggleRegistrationStatus(eventID uint, registrationOpen bool) error {
|
||||
s.logger.Info("Toggling registration status",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Bool("registration_open", registrationOpen),
|
||||
)
|
||||
|
||||
if err := s.eventRepo.UpdateRegistrationStatus(eventID, registrationOpen); err != nil {
|
||||
s.logger.Error("Failed to toggle registration status",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Bool("registration_open", registrationOpen),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("failed to toggle registration status: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Info("Registration status updated successfully",
|
||||
zap.Uint("event_id", eventID),
|
||||
zap.Bool("registration_open", registrationOpen),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// service/jwt_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type JWTService interface {
|
||||
GenerateToken(userID uint, email string) (string, error)
|
||||
ValidateToken(tokenString string) (*jwt.Token, error)
|
||||
ExtractUserID(token *jwt.Token) (uint, error)
|
||||
}
|
||||
|
||||
type jwtService struct {
|
||||
secretKey string
|
||||
}
|
||||
|
||||
func NewJWTService(secretKey string) JWTService {
|
||||
return &jwtService{secretKey: secretKey}
|
||||
}
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func (j *jwtService) GenerateToken(userID uint, email string) (string, error) {
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(j.secretKey))
|
||||
}
|
||||
|
||||
func (j *jwtService) ValidateToken(tokenString string) (*jwt.Token, error) {
|
||||
return jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(j.secretKey), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (j *jwtService) ExtractUserID(token *jwt.Token) (uint, error) {
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok {
|
||||
return 0, errors.New("invalid token claims")
|
||||
}
|
||||
return claims.UserID, nil
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
"errors"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type NewsService interface {
|
||||
CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error)
|
||||
GetNewsByID(id uint) (*models.NewsResponse, error)
|
||||
GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error)
|
||||
UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error)
|
||||
DeleteNews(id uint, userID uint) error
|
||||
IncrementViews(id uint) error
|
||||
CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error)
|
||||
GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error)
|
||||
DeleteComment(commentID, userID uint) error
|
||||
GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error)
|
||||
}
|
||||
|
||||
type newsService struct {
|
||||
newsRepo repository.NewsRepository
|
||||
commentRepo repository.CommentRepository
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewNewsService(newsRepo repository.NewsRepository, commentRepo repository.CommentRepository, log logger.LoggerInterface) NewsService {
|
||||
|
||||
serviceLogger := log.With(zap.String("service", "news"))
|
||||
|
||||
return &newsService{
|
||||
newsRepo: newsRepo,
|
||||
commentRepo: commentRepo,
|
||||
logger: serviceLogger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *newsService) CreateNews(req models.CreateNewsRequest, authorID uint) (*models.NewsResponse, error) {
|
||||
news := &models.News{
|
||||
Title: req.Title,
|
||||
Excerpt: req.Excerpt,
|
||||
Content: req.Content,
|
||||
Image: req.Image,
|
||||
Category: req.Category,
|
||||
AuthorID: authorID,
|
||||
}
|
||||
|
||||
if err := s.newsRepo.Create(news); err != nil {
|
||||
s.logger.Error("Failed to create news", zap.Error(err))
|
||||
return nil, errors.New("failed to create news")
|
||||
}
|
||||
|
||||
// Получаем созданную новость с автором
|
||||
createdNews, err := s.newsRepo.GetByID(news.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toNewsResponse(createdNews), nil
|
||||
}
|
||||
|
||||
func (s *newsService) GetNewsByID(id uint) (*models.NewsResponse, error) {
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
// Увеличиваем счетчик просмотров
|
||||
go s.newsRepo.IncrementViews(id)
|
||||
|
||||
return s.toNewsResponse(news), nil
|
||||
}
|
||||
|
||||
func (s *newsService) GetAllNews(limit, offset int, category string) ([]models.NewsResponse, int64, error) {
|
||||
news, total, err := s.newsRepo.GetAll(limit, offset, category)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
responses := make([]models.NewsResponse, len(news))
|
||||
for i, n := range news {
|
||||
responses[i] = *s.toNewsResponse(&n)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
func (s *newsService) UpdateNews(id uint, req models.UpdateNewsRequest, userID uint) (*models.NewsResponse, error) {
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if news.AuthorID != userID {
|
||||
return nil, errors.New("access denied")
|
||||
}
|
||||
|
||||
// Обновляем поля
|
||||
if req.Title != "" {
|
||||
news.Title = req.Title
|
||||
}
|
||||
if req.Excerpt != "" {
|
||||
news.Excerpt = req.Excerpt
|
||||
}
|
||||
if req.Content != "" {
|
||||
news.Content = req.Content
|
||||
}
|
||||
if req.Image != "" {
|
||||
news.Image = req.Image
|
||||
}
|
||||
if req.Category != "" {
|
||||
news.Category = req.Category
|
||||
}
|
||||
|
||||
if err := s.newsRepo.Update(news); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toNewsResponse(news), nil
|
||||
}
|
||||
|
||||
func (s *newsService) DeleteNews(id uint, userID uint) error {
|
||||
news, err := s.newsRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return errors.New("news not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if news.AuthorID != userID {
|
||||
return errors.New("access denied")
|
||||
}
|
||||
|
||||
return s.newsRepo.Delete(id)
|
||||
}
|
||||
|
||||
func (s *newsService) IncrementViews(id uint) error {
|
||||
return s.newsRepo.IncrementViews(id)
|
||||
}
|
||||
|
||||
func (s *newsService) CreateComment(newsID uint, req models.CreateCommentRequest, authorID uint) (*models.CommentResponse, error) {
|
||||
// Проверяем существование новости
|
||||
_, err := s.newsRepo.GetByID(newsID)
|
||||
if err != nil {
|
||||
return nil, errors.New("news not found")
|
||||
}
|
||||
|
||||
comment := &models.Comment{
|
||||
Content: req.Content,
|
||||
NewsID: newsID,
|
||||
AuthorID: authorID,
|
||||
}
|
||||
|
||||
if err := s.commentRepo.Create(comment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Получаем созданный комментарий с автором
|
||||
createdComment, err := s.commentRepo.GetByID(comment.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toCommentResponse(createdComment), nil
|
||||
}
|
||||
|
||||
func (s *newsService) GetCommentsByNewsID(newsID uint) ([]models.CommentResponse, error) {
|
||||
comments, err := s.commentRepo.GetByNewsID(newsID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responses := make([]models.CommentResponse, len(comments))
|
||||
for i, c := range comments {
|
||||
responses[i] = *s.toCommentResponse(&c)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (s *newsService) DeleteComment(commentID, userID uint) error {
|
||||
comment, err := s.commentRepo.GetByID(commentID)
|
||||
if err != nil {
|
||||
return errors.New("comment not found")
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if comment.AuthorID != userID {
|
||||
return errors.New("access denied")
|
||||
}
|
||||
|
||||
return s.commentRepo.Delete(commentID)
|
||||
}
|
||||
|
||||
func (s *newsService) GetUserNews(userID uint, limit, offset int) ([]models.NewsResponse, int64, error) {
|
||||
news, total, err := s.newsRepo.GetByAuthor(userID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
responses := make([]models.NewsResponse, len(news))
|
||||
for i, n := range news {
|
||||
responses[i] = *s.toNewsResponse(&n)
|
||||
}
|
||||
|
||||
return responses, total, nil
|
||||
}
|
||||
|
||||
// Вспомогательные методы для преобразования
|
||||
func (s *newsService) toNewsResponse(news *models.News) *models.NewsResponse {
|
||||
return &models.NewsResponse{
|
||||
ID: news.ID,
|
||||
CreatedAt: news.CreatedAt,
|
||||
UpdatedAt: news.UpdatedAt,
|
||||
Title: news.Title,
|
||||
Excerpt: news.Excerpt,
|
||||
Content: news.Content,
|
||||
Image: news.Image,
|
||||
Category: news.Category,
|
||||
Views: news.Views,
|
||||
Author: models.AuthorInfo{
|
||||
ID: news.Author.ID,
|
||||
FirstName: news.Author.FirstName,
|
||||
LastName: news.Author.LastName,
|
||||
},
|
||||
Comments: len(news.Comments),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *newsService) toCommentResponse(comment *models.Comment) *models.CommentResponse {
|
||||
return &models.CommentResponse{
|
||||
ID: comment.ID,
|
||||
CreatedAt: comment.CreatedAt,
|
||||
Content: comment.Content,
|
||||
Author: models.AuthorInfo{
|
||||
ID: comment.Author.ID,
|
||||
FirstName: comment.Author.FirstName,
|
||||
LastName: comment.Author.LastName,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// services/personal_best_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PersonalBestService struct {
|
||||
pbRepo repository.PersonalBestRepository
|
||||
userStatsService UserStatsService
|
||||
}
|
||||
|
||||
func NewPersonalBestService(pbRepo repository.PersonalBestRepository, userStatsService UserStatsService) *PersonalBestService {
|
||||
return &PersonalBestService{
|
||||
pbRepo: pbRepo,
|
||||
userStatsService: userStatsService,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if err := s.userStatsService.UpdatePersonalBest(userID, string(req.DistanceType), req.Time); err != nil {
|
||||
// Логируем ошибку, но не прерываем выполнение
|
||||
fmt.Printf("Failed to update user stats: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return personalBest, nil
|
||||
}
|
||||
|
||||
// GetPersonalBestByID возвращает личный рекорд по ID
|
||||
func (s *PersonalBestService) GetPersonalBestByID(id uint) (*models.PersonalBest, error) {
|
||||
return s.pbRepo.GetByID(id)
|
||||
}
|
||||
|
||||
// GetUserPersonalBests возвращает все личные рекорды пользователя
|
||||
func (s *PersonalBestService) GetUserPersonalBests(userID uint) ([]models.PersonalBest, error) {
|
||||
return s.pbRepo.GetByUserID(userID)
|
||||
}
|
||||
|
||||
// GetPersonalBestsByDistance возвращает личные рекорды по дистанции
|
||||
func (s *PersonalBestService) GetPersonalBestsByDistance(userID uint, distanceType models.DistanceType) ([]models.PersonalBest, error) {
|
||||
return s.pbRepo.GetByUserAndDistance(userID, distanceType)
|
||||
}
|
||||
|
||||
// GetBestByDistance возвращает лучший результат на дистанции
|
||||
func (s *PersonalBestService) GetBestByDistance(userID uint, distanceType models.DistanceType) (*models.PersonalBest, error) {
|
||||
return s.pbRepo.GetBestByDistance(userID, distanceType)
|
||||
}
|
||||
|
||||
// UpdatePersonalBest обновляет личный рекорд
|
||||
func (s *PersonalBestService) UpdatePersonalBest(id uint, userID uint, req models.PersonalBestUpdateRequest) (*models.PersonalBest, error) {
|
||||
// Получаем существующий рекорд
|
||||
pb, err := s.pbRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, что рекорд принадлежит пользователю
|
||||
if pb.UserID != userID {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
// Обновляем поля
|
||||
if req.DistanceType != "" {
|
||||
pb.DistanceType = req.DistanceType
|
||||
}
|
||||
if req.Time != "" {
|
||||
pb.Time = req.Time
|
||||
// Пересчитываем темп при изменении времени
|
||||
if req.Pace == "" {
|
||||
calculatedPace, err := s.pbRepo.CalculatePace(req.Time, pb.DistanceType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pb.Pace = calculatedPace
|
||||
}
|
||||
}
|
||||
if req.Pace != "" {
|
||||
pb.Pace = req.Pace
|
||||
}
|
||||
if !req.Date.IsZero() {
|
||||
pb.Date = req.Date
|
||||
}
|
||||
if req.EventName != "" {
|
||||
pb.EventName = req.EventName
|
||||
}
|
||||
if req.Location != "" {
|
||||
pb.Location = req.Location
|
||||
}
|
||||
pb.Verified = req.Verified
|
||||
|
||||
if err := s.pbRepo.Update(pb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pb, nil
|
||||
}
|
||||
|
||||
// DeletePersonalBest удаляет личный рекорд
|
||||
func (s *PersonalBestService) DeletePersonalBest(id uint, userID uint) error {
|
||||
// Проверяем, что рекорд принадлежит пользователю
|
||||
pb, err := s.pbRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pb.UserID != userID {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return s.pbRepo.Delete(id)
|
||||
}
|
||||
|
||||
// GetVerifiedPersonalBests возвращает подтвержденные личные рекорды
|
||||
func (s *PersonalBestService) GetVerifiedPersonalBests(userID uint) ([]models.PersonalBest, error) {
|
||||
return s.pbRepo.GetVerifiedByUserID(userID)
|
||||
}
|
||||
|
||||
// GetPersonalBestsByDateRange возвращает личные рекорды за период
|
||||
func (s *PersonalBestService) GetPersonalBestsByDateRange(userID uint, startDate, endDate time.Time) ([]models.PersonalBest, error) {
|
||||
return s.pbRepo.GetByDateRange(userID, startDate, endDate)
|
||||
}
|
||||
|
||||
// GetRecentPersonalBests возвращает последние личные рекорды
|
||||
func (s *PersonalBestService) GetRecentPersonalBests(userID uint, limit int) ([]models.PersonalBest, error) {
|
||||
return s.pbRepo.GetRecentPersonalBests(userID, limit)
|
||||
}
|
||||
|
||||
// GetPersonalBestsByEvent возвращает личные рекорды по названию события
|
||||
func (s *PersonalBestService) GetPersonalBestsByEvent(userID uint, eventName string) ([]models.PersonalBest, error) {
|
||||
return s.pbRepo.GetByEventName(userID, eventName)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// CalculatePace вычисляет темп для времени и дистанции
|
||||
func (s *PersonalBestService) CalculatePace(timeStr string, distanceType models.DistanceType) (string, error) {
|
||||
return s.pbRepo.CalculatePace(timeStr, distanceType)
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// service/review_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
"errors"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type ReviewService interface {
|
||||
CreateReview(req *models.CreateReviewRequest, authorID uint) (*models.ReviewResponse, error)
|
||||
GetReviewByID(id uint) (*models.ReviewResponse, error)
|
||||
GetAllReviews(page, limit int, sortBy, filter string) ([]models.ReviewResponse, int, error)
|
||||
GetUserReviews(userID uint) ([]models.ReviewResponse, error)
|
||||
UpdateReview(id uint, req *models.UpdateReviewRequest, userID uint, isAdmin bool) (*models.ReviewResponse, error)
|
||||
DeleteReview(id uint, userID uint, isAdmin bool) error
|
||||
GetReviewsStats() (*models.ReviewsStatsResponse, error)
|
||||
}
|
||||
|
||||
type reviewService struct {
|
||||
reviewRepo repository.ReviewRepository
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewReviewService(reviewRepo repository.ReviewRepository, logger logger.LoggerInterface) ReviewService {
|
||||
return &reviewService{
|
||||
reviewRepo: reviewRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *reviewService) CreateReview(req *models.CreateReviewRequest, authorID uint) (*models.ReviewResponse, error) {
|
||||
review := &models.Review{
|
||||
Rating: req.Rating,
|
||||
Text: req.Text,
|
||||
Achievement: req.Achievement,
|
||||
Distance: req.Distance,
|
||||
Improvement: req.Improvement,
|
||||
Trainings: req.Trainings,
|
||||
AuthorID: authorID,
|
||||
Verified: false, // По умолчанию непроверенный
|
||||
}
|
||||
|
||||
if err := s.reviewRepo.Create(review); err != nil {
|
||||
s.logger.Error("Failed to create review", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Получаем созданный отзыв с информацией об авторе
|
||||
createdReview, err := s.reviewRepo.GetByID(review.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get created review", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toReviewResponse(createdReview), nil
|
||||
}
|
||||
|
||||
func (s *reviewService) GetReviewByID(id uint) (*models.ReviewResponse, error) {
|
||||
review, err := s.reviewRepo.GetByID(id)
|
||||
if err != nil {
|
||||
s.logger.With(zap.Int("id", int(id))).Error("Failed to get review by ID", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toReviewResponse(review), nil
|
||||
}
|
||||
|
||||
func (s *reviewService) GetAllReviews(page, limit int, sortBy, filter string) ([]models.ReviewResponse, int, error) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
reviews, total, err := s.reviewRepo.GetAll(page, limit, sortBy, filter)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get all reviews", zap.Error(err))
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
responses := make([]models.ReviewResponse, len(reviews))
|
||||
for i, review := range reviews {
|
||||
responses[i] = *s.toReviewResponse(&review)
|
||||
}
|
||||
|
||||
totalPages := (int(total) + limit - 1) / limit
|
||||
|
||||
return responses, totalPages, nil
|
||||
}
|
||||
|
||||
func (s *reviewService) GetUserReviews(userID uint) ([]models.ReviewResponse, error) {
|
||||
reviews, err := s.reviewRepo.GetByAuthorID(userID)
|
||||
if err != nil {
|
||||
s.logger.With(zap.Int("userID", int(userID))).Error("Failed to get user reviews", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responses := make([]models.ReviewResponse, len(reviews))
|
||||
for i, review := range reviews {
|
||||
responses[i] = *s.toReviewResponse(&review)
|
||||
}
|
||||
|
||||
return responses, nil
|
||||
}
|
||||
|
||||
func (s *reviewService) UpdateReview(id uint, req *models.UpdateReviewRequest, userID uint, isAdmin bool) (*models.ReviewResponse, error) {
|
||||
review, err := s.reviewRepo.GetByID(id)
|
||||
if err != nil {
|
||||
s.logger.With(zap.Int("id", int(id))).Error("Failed to get review for update", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if review.AuthorID != userID && !isAdmin {
|
||||
s.logger.With(zap.Int("userID", int(userID))).With(zap.Int("reviewAuthorID", int(review.AuthorID))).Error("Unauthorized attempt to update review", zap.Error(err))
|
||||
}
|
||||
|
||||
// Обновляем поля
|
||||
if req.Rating != 0 {
|
||||
review.Rating = req.Rating
|
||||
}
|
||||
if req.Text != "" {
|
||||
review.Text = req.Text
|
||||
}
|
||||
if req.Achievement != "" {
|
||||
review.Achievement = req.Achievement
|
||||
}
|
||||
if req.Distance != "" {
|
||||
review.Distance = req.Distance
|
||||
}
|
||||
if req.Improvement != "" {
|
||||
review.Improvement = req.Improvement
|
||||
}
|
||||
if req.Trainings != 0 {
|
||||
review.Trainings = req.Trainings
|
||||
}
|
||||
|
||||
if err := s.reviewRepo.Update(review); err != nil {
|
||||
s.logger.With(zap.Int("id", int(id))).Error("Failed to update review", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Получаем обновленный отзыв
|
||||
updatedReview, err := s.reviewRepo.GetByID(id)
|
||||
if err != nil {
|
||||
s.logger.With(zap.Int("id", int(id))).Error("Failed to get updated review", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toReviewResponse(updatedReview), nil
|
||||
}
|
||||
|
||||
func (s *reviewService) DeleteReview(id uint, userID uint, isAdmin bool) error {
|
||||
review, err := s.reviewRepo.GetByID(id)
|
||||
if err != nil {
|
||||
s.logger.With(zap.Int("id", int(id))).Error("Failed to get review for deletion", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
if review.AuthorID != userID && !isAdmin {
|
||||
s.logger.With(zap.Int("userID", int(userID))).With(zap.Int("reviewAuthorID", int(review.AuthorID))).Error("Unauthorized attempt to delete review", zap.Error(err))
|
||||
return errors.New("unauthorized")
|
||||
}
|
||||
|
||||
return s.reviewRepo.Delete(id)
|
||||
}
|
||||
|
||||
func (s *reviewService) GetReviewsStats() (*models.ReviewsStatsResponse, error) {
|
||||
return s.reviewRepo.GetStats()
|
||||
}
|
||||
|
||||
func (s *reviewService) toReviewResponse(review *models.Review) *models.ReviewResponse {
|
||||
return &models.ReviewResponse{
|
||||
ID: review.ID,
|
||||
CreatedAt: review.CreatedAt,
|
||||
Rating: review.Rating,
|
||||
Text: review.Text,
|
||||
Achievement: review.Achievement,
|
||||
Distance: review.Distance,
|
||||
Improvement: review.Improvement,
|
||||
Trainings: review.Trainings,
|
||||
Verified: review.Verified,
|
||||
Author: models.AuthorInfo{
|
||||
ID: review.Author.ID,
|
||||
FirstName: review.Author.FirstName,
|
||||
LastName: review.Author.LastName,
|
||||
Email: review.Author.Email,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
// service/training_plan_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type TrainingPlanService interface {
|
||||
CreateTrainingPlan(userID uint, req *models.TrainingPlanCreateRequest) (*models.TrainingPlan, error)
|
||||
GetTrainingPlansByUserID(userID uint) ([]models.TrainingPlan, error)
|
||||
GetTrainingPlanByID(userID uint, planID uint) (*models.TrainingPlan, error)
|
||||
UpdateTrainingPlan(userID uint, planID uint, req *models.TrainingPlanUpdateRequest) (*models.TrainingPlan, error)
|
||||
DeleteTrainingPlan(userID uint, planID uint) error
|
||||
GetActiveTrainingPlan(userID uint) (*models.TrainingPlan, error)
|
||||
MarkTrainingPlanAsCompleted(userID uint, planID uint) error
|
||||
UpdateCurrentWeek(userID uint, planID uint, currentWeek int) error
|
||||
}
|
||||
|
||||
type trainingPlanService struct {
|
||||
trainingPlanRepo repository.TrainingPlanRepository
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewTrainingPlanService(trainingPlanRepo repository.TrainingPlanRepository) TrainingPlanService {
|
||||
return &trainingPlanService{
|
||||
trainingPlanRepo: trainingPlanRepo,
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("service", "training_plan"))),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTrainingPlan создает новый план тренировок
|
||||
func (s *trainingPlanService) CreateTrainingPlan(userID uint, req *models.TrainingPlanCreateRequest) (*models.TrainingPlan, error) {
|
||||
s.logger.Debug("creating training plan",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("title", req.Title),
|
||||
)
|
||||
|
||||
plan := &models.TrainingPlan{
|
||||
UserID: userID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Weeks: req.Weeks,
|
||||
WorkoutsPerWeek: req.WorkoutsPerWeek,
|
||||
TargetDistance: req.TargetDistance,
|
||||
TargetDate: req.TargetDate,
|
||||
CurrentWeek: 1,
|
||||
Completed: false,
|
||||
}
|
||||
|
||||
if err := s.trainingPlanRepo.Create(plan); err != nil {
|
||||
s.logger.Error("failed to create training plan in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("training plan created successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", plan.ID),
|
||||
)
|
||||
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// GetTrainingPlansByUserID возвращает все планы тренировок пользователя
|
||||
func (s *trainingPlanService) GetTrainingPlansByUserID(userID uint) ([]models.TrainingPlan, error) {
|
||||
s.logger.Debug("getting training plans for user", zap.Uint("user_id", userID))
|
||||
|
||||
plans, err := s.trainingPlanRepo.GetByUserID(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get training plans from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("training plans retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("count", len(plans)),
|
||||
)
|
||||
|
||||
return plans, nil
|
||||
}
|
||||
|
||||
// GetTrainingPlanByID возвращает план тренировок по ID
|
||||
func (s *trainingPlanService) GetTrainingPlanByID(userID uint, planID uint) (*models.TrainingPlan, error) {
|
||||
s.logger.Debug("getting training plan by ID",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
)
|
||||
|
||||
plan, err := s.trainingPlanRepo.GetByID(planID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get training plan from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, что план принадлежит пользователю
|
||||
if plan.UserID != userID {
|
||||
s.logger.Warn("training plan access denied - user mismatch",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_user_id", plan.UserID),
|
||||
zap.Uint("plan_id", planID),
|
||||
)
|
||||
return nil, repository.ErrNotFound
|
||||
}
|
||||
|
||||
s.logger.Debug("training plan retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
)
|
||||
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// UpdateTrainingPlan обновляет план тренировок
|
||||
func (s *trainingPlanService) UpdateTrainingPlan(userID uint, planID uint, req *models.TrainingPlanUpdateRequest) (*models.TrainingPlan, error) {
|
||||
s.logger.Debug("updating training plan",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
)
|
||||
|
||||
// Сначала получаем существующий план
|
||||
plan, err := s.GetTrainingPlanByID(userID, planID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Обновляем только переданные поля
|
||||
if req.Title != "" {
|
||||
plan.Title = req.Title
|
||||
}
|
||||
if req.Description != "" {
|
||||
plan.Description = req.Description
|
||||
}
|
||||
if req.Weeks > 0 {
|
||||
plan.Weeks = req.Weeks
|
||||
}
|
||||
if req.WorkoutsPerWeek > 0 {
|
||||
plan.WorkoutsPerWeek = req.WorkoutsPerWeek
|
||||
}
|
||||
if req.TargetDistance != "" {
|
||||
plan.TargetDistance = req.TargetDistance
|
||||
}
|
||||
if !req.TargetDate.IsZero() {
|
||||
plan.TargetDate = req.TargetDate
|
||||
}
|
||||
|
||||
// Сохраняем обновления
|
||||
if err := s.trainingPlanRepo.Update(plan); err != nil {
|
||||
s.logger.Error("failed to update training plan in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("training plan updated successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
)
|
||||
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// DeleteTrainingPlan удаляет план тренировок
|
||||
func (s *trainingPlanService) DeleteTrainingPlan(userID uint, planID uint) error {
|
||||
s.logger.Debug("deleting training plan",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
)
|
||||
|
||||
// Проверяем, что план существует и принадлежит пользователю
|
||||
_, err := s.GetTrainingPlanByID(userID, planID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Удаляем план
|
||||
if err := s.trainingPlanRepo.Delete(planID); err != nil {
|
||||
s.logger.Error("failed to delete training plan from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Debug("training plan deleted successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveTrainingPlan возвращает активный план тренировок пользователя
|
||||
func (s *trainingPlanService) GetActiveTrainingPlan(userID uint) (*models.TrainingPlan, error) {
|
||||
s.logger.Debug("getting active training plan for user", zap.Uint("user_id", userID))
|
||||
|
||||
plan, err := s.trainingPlanRepo.GetActivePlan(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get active training plan from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("active training plan retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", plan.ID),
|
||||
)
|
||||
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// MarkTrainingPlanAsCompleted помечает план тренировок как завершенный
|
||||
func (s *trainingPlanService) MarkTrainingPlanAsCompleted(userID uint, planID uint) error {
|
||||
s.logger.Debug("marking training plan as completed",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
)
|
||||
|
||||
// Проверяем, что план существует и принадлежит пользователю
|
||||
_, err := s.GetTrainingPlanByID(userID, planID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Помечаем как завершенный
|
||||
if err := s.trainingPlanRepo.MarkAsCompleted(planID); err != nil {
|
||||
s.logger.Error("failed to mark training plan as completed in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Debug("training plan marked as completed successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateCurrentWeek обновляет текущую неделю плана тренировок
|
||||
func (s *trainingPlanService) UpdateCurrentWeek(userID uint, planID uint, currentWeek int) error {
|
||||
s.logger.Debug("updating current week for training plan",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
zap.Int("current_week", currentWeek),
|
||||
)
|
||||
|
||||
// Проверяем, что план существует и принадлежит пользователю
|
||||
_, err := s.GetTrainingPlanByID(userID, planID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Обновляем текущую неделю
|
||||
if err := s.trainingPlanRepo.UpdateCurrentWeek(planID, currentWeek); err != nil {
|
||||
s.logger.Error("failed to update current week in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Debug("current week updated successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("plan_id", planID),
|
||||
zap.Int("current_week", currentWeek),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type UserService interface {
|
||||
GetUserProfile(userID uint) (*models.User, error)
|
||||
UpdateProfile(user *models.User) error
|
||||
GetAllUsers() ([]models.User, error)
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
userRepo repository.UserRepository
|
||||
jwtService JWTService
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
// Обновление профиля
|
||||
func (s *userService) UpdateProfile(user *models.User) error {
|
||||
s.logger.Info("Updating user profile",
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
// Проверяем, что пользователь существует
|
||||
existingUser, err := s.userRepo.FindByID(user.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("User not found for profile update",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// Убеждаемся, что email не меняется
|
||||
user.Email = existingUser.Email
|
||||
user.Avatar = existingUser.Avatar
|
||||
|
||||
updateData := &models.User{
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Avatar: user.Avatar,
|
||||
Phone: user.Phone,
|
||||
Experience: user.Experience,
|
||||
Goals: user.Goals,
|
||||
Newsletter: user.Newsletter,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
return s.userRepo.UpdateExcludeEmail(updateData)
|
||||
}
|
||||
|
||||
func NewUserService(userRepo repository.UserRepository, jwtService JWTService, log logger.LoggerInterface) userService {
|
||||
// Создаем логгер с контекстом для сервиса
|
||||
serviceLogger := log.With(zap.String("service", "user"))
|
||||
|
||||
return userService{
|
||||
userRepo: userRepo,
|
||||
jwtService: jwtService,
|
||||
logger: serviceLogger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *userService) GetAllUsers() ([]models.User, error) {
|
||||
s.logger.Info("Fetching all users")
|
||||
|
||||
users, err := s.userRepo.FindAll()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to fetch users",
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("failed to get users: %w", err)
|
||||
}
|
||||
|
||||
s.logger.Debug("Successfully fetched users",
|
||||
zap.Int("count", len(users)),
|
||||
)
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (s *authService) UpdateProfile(user *models.User) error {
|
||||
s.logger.Info("Updating user profile",
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
// Проверяем, что пользователь существует
|
||||
existingUser, err := s.userRepo.FindByID(user.ID)
|
||||
if err != nil {
|
||||
s.logger.Error("User not found for profile update",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// Убеждаемся, что email не меняется
|
||||
user.Email = existingUser.Email
|
||||
user.Avatar = existingUser.Avatar
|
||||
|
||||
updateData := &models.User{
|
||||
ID: user.ID,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Avatar: user.Avatar,
|
||||
Phone: user.Phone,
|
||||
Experience: user.Experience,
|
||||
Goals: user.Goals,
|
||||
Newsletter: user.Newsletter,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
return s.userRepo.UpdateExcludeEmail(updateData)
|
||||
}
|
||||
|
||||
func (s *userService) GetUserProfile(userID uint) (*models.User, error) {
|
||||
s.logger.Debug("Getting user profile",
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
user, err := s.userRepo.FindByID(userID)
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get user profile",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
// service/user_stats_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type UserStatsService interface {
|
||||
GetUserStats(userID uint) (*models.UserStatsResponse, error)
|
||||
UpdatePersonalBest(userID uint, distanceType string, time string) error
|
||||
IncrementWorkout(userID uint, distance float64, duration int) error
|
||||
ResetWeeklyDistance(userID uint) error
|
||||
ResetMonthlyDistance(userID uint) error
|
||||
CreateUserStats(userID uint) error
|
||||
}
|
||||
|
||||
type userStatsService struct {
|
||||
logger logger.LoggerInterface
|
||||
userStatsRepo repository.UserStatsRepository
|
||||
}
|
||||
|
||||
func NewUserStatsService(userStatsRepo repository.UserStatsRepository) UserStatsService {
|
||||
return &userStatsService{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("service", "user_stats"))),
|
||||
userStatsRepo: userStatsRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserStats возвращает статистику пользователя в формате DTO
|
||||
func (s *userStatsService) GetUserStats(userID uint) (*models.UserStatsResponse, error) {
|
||||
s.logger.Info("getting user stats",
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
stats, err := s.userStatsRepo.GetUserStatsResponse(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get user stats from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("user stats retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Float64("total_distance", stats.TotalDistance),
|
||||
zap.Int("workouts_count", stats.WorkoutsCount),
|
||||
)
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// UpdatePersonalBest обновляет личный рекорд пользователя
|
||||
func (s *userStatsService) UpdatePersonalBest(userID uint, distanceType string, time string) error {
|
||||
s.logger.Info("updating personal best",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("distance_type", distanceType),
|
||||
zap.String("time", time),
|
||||
)
|
||||
|
||||
// Используем GetByUserIDOrCreate вместо проверки существования
|
||||
_, err := s.userStatsRepo.GetByUserIDOrCreate(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get or create user stats",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.userStatsRepo.UpdatePersonalBest(userID, distanceType, time); err != nil {
|
||||
s.logger.Error("failed to update personal best in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("distance_type", distanceType),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("personal best updated successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("distance_type", distanceType),
|
||||
zap.String("time", time),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IncrementWorkout увеличивает счетчик тренировок и обновляет статистику
|
||||
func (s *userStatsService) IncrementWorkout(userID uint, distance float64, duration int) error {
|
||||
s.logger.Info("incrementing workout stats",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Float64("distance", distance),
|
||||
zap.Int("duration", duration),
|
||||
)
|
||||
|
||||
// Используем GetByUserIDOrCreate для гарантии существования статистики
|
||||
_, err := s.userStatsRepo.GetByUserIDOrCreate(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get or create user stats",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Обновляем серии тренировок
|
||||
currentTime := time.Now()
|
||||
if err := s.userStatsRepo.UpdateStreaks(userID, currentTime); err != nil {
|
||||
s.logger.Error("failed to update streaks in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Обновляем недельный и месячный пробег
|
||||
if err := s.userStatsRepo.UpdateWeeklyDistance(userID, distance); err != nil {
|
||||
s.logger.Error("failed to update weekly distance in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.userStatsRepo.UpdateMonthlyDistance(userID, distance); err != nil {
|
||||
s.logger.Error("failed to update monthly distance in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// Увеличиваем счетчик тренировок и обновляем общие показатели
|
||||
if err := s.userStatsRepo.IncrementWorkouts(userID, distance, duration); err != nil {
|
||||
s.logger.Error("failed to increment workouts in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("workout stats incremented successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Float64("distance", distance),
|
||||
zap.Int("duration", duration),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetWeeklyDistance сбрасывает недельный пробег
|
||||
func (s *userStatsService) ResetWeeklyDistance(userID uint) error {
|
||||
s.logger.Info("resetting weekly distance",
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
userStats, err := s.userStatsRepo.GetByUserID(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get user stats for weekly reset",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
userStats.WeeklyDistance = 0
|
||||
if err := s.userStatsRepo.Update(userStats); err != nil {
|
||||
s.logger.Error("failed to reset weekly distance in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("weekly distance reset successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetMonthlyDistance сбрасывает месячный пробег
|
||||
func (s *userStatsService) ResetMonthlyDistance(userID uint) error {
|
||||
s.logger.Info("resetting monthly distance",
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
userStats, err := s.userStatsRepo.GetByUserID(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get user stats for monthly reset",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
userStats.MonthlyDistance = 0
|
||||
if err := s.userStatsRepo.Update(userStats); err != nil {
|
||||
s.logger.Error("failed to reset monthly distance in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("monthly distance reset successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateUserStats создает начальную статистику для пользователя
|
||||
func (s *userStatsService) CreateUserStats(userID uint) error {
|
||||
s.logger.Info("creating user stats",
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
userStats := &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 := s.userStatsRepo.Create(userStats); err != nil {
|
||||
s.logger.Error("failed to create user stats in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("user stats created successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// service/user_workout_service.go
|
||||
package service
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type WorkoutService interface {
|
||||
CreateWorkout(userID uint, req *models.WorkoutCreateRequest) (*models.Workout, error)
|
||||
GetUserWorkouts(userID uint) ([]models.Workout, error)
|
||||
GetWorkoutByID(userID uint, workoutID uint) (*models.Workout, error)
|
||||
UpdateWorkout(userID uint, workoutID uint, req *models.WorkoutUpdateRequest) (*models.Workout, error)
|
||||
DeleteWorkout(userID uint, workoutID uint) error
|
||||
GetWorkoutStats(userID uint) (*models.WorkoutStatsResponse, error)
|
||||
GetWorkoutsByType(userID uint, workoutType models.WorkoutType) ([]models.Workout, error)
|
||||
GetLatestWorkouts(userID uint, limit int) ([]models.Workout, error)
|
||||
}
|
||||
|
||||
type workoutService struct {
|
||||
workoutRepo repository.WorkoutRepository
|
||||
logger logger.LoggerInterface
|
||||
}
|
||||
|
||||
func NewWorkoutService(workoutRepo repository.WorkoutRepository) WorkoutService {
|
||||
return &workoutService{
|
||||
workoutRepo: workoutRepo,
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("service", "workout"))),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWorkout создает новую тренировку
|
||||
func (s *workoutService) CreateWorkout(userID uint, req *models.WorkoutCreateRequest) (*models.Workout, error) {
|
||||
s.logger.Info("creating new workout",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("type", string(req.Type)),
|
||||
zap.Float64("distance", req.Distance),
|
||||
)
|
||||
|
||||
// Создаем модель тренировки
|
||||
workout := &models.Workout{
|
||||
UserID: userID,
|
||||
Type: req.Type,
|
||||
Distance: req.Distance,
|
||||
Duration: req.Duration,
|
||||
Pace: req.Pace,
|
||||
Calories: req.Calories,
|
||||
Notes: req.Notes,
|
||||
Date: req.Date,
|
||||
}
|
||||
|
||||
// Сохраняем в репозитории
|
||||
if err := s.workoutRepo.Create(workout); err != nil {
|
||||
s.logger.Error("failed to create workout in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("workout created successfully",
|
||||
zap.Uint("workout_id", workout.ID),
|
||||
zap.Uint("user_id", userID),
|
||||
)
|
||||
|
||||
return workout, nil
|
||||
}
|
||||
|
||||
// GetUserWorkouts возвращает все тренировки пользователя
|
||||
func (s *workoutService) GetUserWorkouts(userID uint) ([]models.Workout, error) {
|
||||
s.logger.Debug("getting user workouts", zap.Uint("user_id", userID))
|
||||
|
||||
workouts, err := s.workoutRepo.FindByUserID(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get user workouts from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("retrieved user workouts",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("count", len(workouts)),
|
||||
)
|
||||
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
// GetWorkoutByID возвращает тренировку по ID
|
||||
func (s *workoutService) GetWorkoutByID(userID uint, workoutID uint) (*models.Workout, error) {
|
||||
s.logger.Debug("getting workout by ID",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
workout, err := s.workoutRepo.FindByID(workoutID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get workout from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Проверяем, что тренировка принадлежит пользователю
|
||||
if workout.UserID != userID {
|
||||
s.logger.Warn("workout access denied - user mismatch",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_user_id", workout.UserID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
return nil, repository.ErrNotFound
|
||||
}
|
||||
|
||||
s.logger.Debug("workout retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
return workout, nil
|
||||
}
|
||||
|
||||
// UpdateWorkout обновляет тренировку
|
||||
func (s *workoutService) UpdateWorkout(userID uint, workoutID uint, req *models.WorkoutUpdateRequest) (*models.Workout, error) {
|
||||
s.logger.Info("updating workout",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
// Сначала получаем существующую тренировку
|
||||
workout, err := s.GetWorkoutByID(userID, workoutID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Обновляем только переданные поля
|
||||
if req.Type != "" {
|
||||
workout.Type = req.Type
|
||||
}
|
||||
if req.Distance > 0 {
|
||||
workout.Distance = req.Distance
|
||||
}
|
||||
if req.Duration > 0 {
|
||||
workout.Duration = req.Duration
|
||||
}
|
||||
if req.Pace != "" {
|
||||
workout.Pace = req.Pace
|
||||
}
|
||||
if req.Calories > 0 {
|
||||
workout.Calories = req.Calories
|
||||
}
|
||||
if req.Notes != "" {
|
||||
workout.Notes = req.Notes
|
||||
}
|
||||
if !req.Date.IsZero() {
|
||||
workout.Date = req.Date
|
||||
}
|
||||
|
||||
// Сохраняем обновления
|
||||
if err := s.workoutRepo.Update(workout); err != nil {
|
||||
s.logger.Error("failed to update workout in repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("workout updated successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
return workout, nil
|
||||
}
|
||||
|
||||
// DeleteWorkout удаляет тренировку
|
||||
func (s *workoutService) DeleteWorkout(userID uint, workoutID uint) error {
|
||||
s.logger.Info("deleting workout",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
// Проверяем, что тренировка существует и принадлежит пользователю
|
||||
workout, err := s.GetWorkoutByID(userID, workoutID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Удаляем тренировку
|
||||
if err := s.workoutRepo.Delete(workout.ID); err != nil {
|
||||
s.logger.Error("failed to delete workout from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("workout deleted successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Uint("workout_id", workoutID),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWorkoutStats возвращает статистику тренировок
|
||||
func (s *workoutService) GetWorkoutStats(userID uint) (*models.WorkoutStatsResponse, error) {
|
||||
s.logger.Debug("getting workout stats", zap.Uint("user_id", userID))
|
||||
|
||||
stats, err := s.workoutRepo.GetWorkoutStats(userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get workout stats from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("workout stats retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("total_workouts", stats.TotalWorkouts),
|
||||
zap.Float64("total_distance", stats.TotalDistance),
|
||||
)
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetWorkoutsByType возвращает тренировки по типу
|
||||
func (s *workoutService) GetWorkoutsByType(userID uint, workoutType models.WorkoutType) ([]models.Workout, error) {
|
||||
s.logger.Debug("getting workouts by type",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("type", string(workoutType)),
|
||||
)
|
||||
|
||||
workouts, err := s.workoutRepo.GetByType(userID, workoutType)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get workouts by type from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("type", string(workoutType)),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("workouts by type retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.String("type", string(workoutType)),
|
||||
zap.Int("count", len(workouts)),
|
||||
)
|
||||
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
// GetLatestWorkouts возвращает последние тренировки
|
||||
func (s *workoutService) GetLatestWorkouts(userID uint, limit int) ([]models.Workout, error) {
|
||||
s.logger.Debug("getting latest workouts",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("limit", limit),
|
||||
)
|
||||
|
||||
workouts, err := s.workoutRepo.GetLatestWorkouts(userID, limit)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get latest workouts from repository",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("limit", limit),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("latest workouts retrieved successfully",
|
||||
zap.Uint("user_id", userID),
|
||||
zap.Int("limit", limit),
|
||||
zap.Int("count", len(workouts)),
|
||||
)
|
||||
|
||||
return workouts, nil
|
||||
}
|
||||
Reference in New Issue
Block a user