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:
2025-10-24 05:22:44 +05:00
parent 358c14428f
commit 15357fd3c0
211 changed files with 3 additions and 3 deletions
@@ -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
}