create and moove into new directories for BegushiyBashkir and
yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarbacreate and moove into new directories for BegushiyBashkir and yalarba
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