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,108 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"api_bb/internal/config"
|
||||
"api_bb/internal/database"
|
||||
"api_bb/internal/routes"
|
||||
"api_bb/pkg/logger"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
cfg *config.Config
|
||||
db *database.Database
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func NewApp(cfg *config.Config) *App {
|
||||
return &App{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize инициализирует приложение (БД, миграции, роутинг)
|
||||
func (a *App) Initialize() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
// Инициализация базы данных
|
||||
dbConfig := &database.Config{
|
||||
URL: a.cfg.DatabaseURL,
|
||||
}
|
||||
a.db = database.NewDatabase(dbConfig)
|
||||
|
||||
// Подключение к БД
|
||||
if err := a.db.Connect(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Проверка соединения
|
||||
if err := a.db.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Выполнение миграций
|
||||
if err := a.db.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Настройка роутера
|
||||
router := routes.SetupRouter(a.db.DB, a.cfg)
|
||||
|
||||
// Настройка HTTP сервера
|
||||
a.server = &http.Server{
|
||||
Addr: ":" + a.cfg.Port,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
zapLogger.Info("application initialized successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start запускает HTTP сервер
|
||||
func (a *App) Start() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
zapLogger.Info("starting HTTP server", zap.String("port", a.cfg.Port))
|
||||
|
||||
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown gracefully останавливает приложение
|
||||
func (a *App) Shutdown() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
zapLogger.Info("shutdown signal received")
|
||||
|
||||
// Graceful shutdown сервера
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
a.server.SetKeepAlivesEnabled(false)
|
||||
if err := a.server.Shutdown(ctx); err != nil {
|
||||
zapLogger.Error("could not gracefully shutdown the server", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// Закрытие соединения с БД
|
||||
if err := a.db.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
zapLogger.Info("application shutdown completed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDB возвращает экземпляр базы данных
|
||||
func (a *App) GetDB() *gorm.DB {
|
||||
return a.db.DB
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// config/config.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
DatabaseURL string
|
||||
StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"`
|
||||
JWTSecret string `env:"JWT_SECRET,required"`
|
||||
|
||||
// Email configuration
|
||||
SMTPHost string `env:"SMTP_HOST,required"`
|
||||
SMTPPort int `env:"SMTP_PORT,required"`
|
||||
SMTPUsername string `env:"SMTP_USERNAME,required"`
|
||||
SMTPPassword string `env:"SMTP_PASSWORD,required"`
|
||||
FromEmail string `env:"FROM_EMAIL,required"`
|
||||
FrontendURL string `env:"FRONTEND_URL,required"`
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
_ = godotenv.Load(".env")
|
||||
port := getEnv("PORT", "8080")
|
||||
jwtSecret := getEnv("JWT_SECRET", "your-secret-key")
|
||||
|
||||
// Формируем DSN для PostgreSQL из переменных окружения
|
||||
databaseURL := getPostgresDSN()
|
||||
|
||||
return &Config{
|
||||
Port: port,
|
||||
DatabaseURL: databaseURL,
|
||||
JWTSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func getPostgresDSN() string {
|
||||
host := getEnv("DB_HOST", "localhost")
|
||||
port := getEnv("DB_PORT", "5432")
|
||||
user := getEnv("DB_USER", "postgres")
|
||||
password := getEnv("DB_PASSWORD", "postgres")
|
||||
dbname := getEnv("DB_NAME", "bb_db")
|
||||
sslmode := getEnv("DB_SSLMODE", "disable")
|
||||
|
||||
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
|
||||
host, port, user, password, dbname, sslmode)
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"api_bb/pkg/logger"
|
||||
)
|
||||
|
||||
type Database struct {
|
||||
DB *gorm.DB
|
||||
cfg *Config
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
func NewDatabase(cfg *Config) *Database {
|
||||
return &Database{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect устанавливает соединение с базой данных
|
||||
func (d *Database) Connect() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
// Логирование попытки подключения к БД
|
||||
zapLogger.Info("attempting to connect to database",
|
||||
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
||||
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
||||
)
|
||||
|
||||
db, err := gorm.Open(postgres.Open(d.cfg.URL), &gorm.Config{})
|
||||
if err != nil {
|
||||
zapLogger.Error("failed to connect to database",
|
||||
zap.Error(err),
|
||||
zap.String("database_url", MaskPassword(d.cfg.URL)),
|
||||
)
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
d.DB = db
|
||||
|
||||
// Логирование успешного подключения к БД
|
||||
zapLogger.Info("successfully connected to database",
|
||||
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
||||
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ping проверяет соединение с базой данных
|
||||
func (d *Database) Ping() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
sqlDB, err := d.DB.DB()
|
||||
if err != nil {
|
||||
zapLogger.Error("failed to get database instance", zap.Error(err))
|
||||
return fmt.Errorf("failed to get database instance: %w", err)
|
||||
}
|
||||
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
zapLogger.Error("database ping failed", zap.Error(err))
|
||||
return fmt.Errorf("database ping failed: %w", err)
|
||||
}
|
||||
|
||||
zapLogger.Info("database ping successful")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close закрывает соединение с базой данных
|
||||
func (d *Database) Close() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
if d.DB == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sqlDB, err := d.DB.DB()
|
||||
if err != nil {
|
||||
zapLogger.Error("failed to get database instance for closing", zap.Error(err))
|
||||
return fmt.Errorf("failed to get database instance: %w", err)
|
||||
}
|
||||
|
||||
zapLogger.Info("closing database connection")
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
zapLogger.Error("failed to close database connection", zap.Error(err))
|
||||
return fmt.Errorf("failed to close database connection: %w", err)
|
||||
}
|
||||
|
||||
zapLogger.Info("database connection closed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Вспомогательные функции для работы с DSN
|
||||
|
||||
// ExtractHostFromDSN извлекает хост из DSN строки
|
||||
func ExtractHostFromDSN(dsn string) string {
|
||||
// Простая реализация для PostgreSQL DSN
|
||||
parts := strings.Split(dsn, " ")
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "host=") {
|
||||
return strings.TrimPrefix(part, "host=")
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// ExtractDBNameFromDSN извлекает имя базы данных из DSN строки
|
||||
func ExtractDBNameFromDSN(dsn string) string {
|
||||
// Простая реализация для PostgreSQL DSN
|
||||
parts := strings.Split(dsn, " ")
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "dbname=") {
|
||||
return strings.TrimPrefix(part, "dbname=")
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// MaskPassword маскирует пароль в DSN строке для безопасного логирования
|
||||
func MaskPassword(dsn string) string {
|
||||
// Простая реализация - заменяет пароль на ***
|
||||
parts := strings.Split(dsn, " ")
|
||||
for i, part := range parts {
|
||||
if strings.HasPrefix(part, "password=") {
|
||||
parts[i] = "password=***"
|
||||
break
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/pkg/logger"
|
||||
)
|
||||
|
||||
// Migrate выполняет автоматические миграции для всех моделей
|
||||
func (d *Database) Migrate() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
zapLogger.Info("starting database migration")
|
||||
|
||||
// Список всех моделей для миграции
|
||||
models := []interface{}{
|
||||
&models.User{},
|
||||
&models.News{},
|
||||
&models.Comment{},
|
||||
&models.Review{},
|
||||
&models.UserStats{},
|
||||
&models.Workout{},
|
||||
&models.Achievement{},
|
||||
&models.Event{},
|
||||
&models.EventRegistration{},
|
||||
&models.PersonalBest{},
|
||||
&models.TrainingPlan{},
|
||||
&models.EmailVerification{},
|
||||
// Добавьте другие модели здесь
|
||||
}
|
||||
|
||||
for _, model := range models {
|
||||
modelName := getModelName(model)
|
||||
zapLogger.Debug("migrating model", zap.String("model", modelName))
|
||||
|
||||
if err := d.DB.AutoMigrate(model); err != nil {
|
||||
zapLogger.Error("failed to migrate model",
|
||||
zap.String("model", modelName),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
zapLogger.Info("database migration completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateModels выполняет миграции для конкретных моделей
|
||||
func (d *Database) MigrateModels(models ...interface{}) error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
zapLogger.Info("starting migration for specific models",
|
||||
zap.Int("model_count", len(models)),
|
||||
)
|
||||
|
||||
for _, model := range models {
|
||||
modelName := getModelName(model)
|
||||
zapLogger.Debug("migrating model", zap.String("model", modelName))
|
||||
|
||||
if err := d.DB.AutoMigrate(model); err != nil {
|
||||
zapLogger.Error("failed to migrate model",
|
||||
zap.String("model", modelName),
|
||||
zap.Error(err),
|
||||
)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
zapLogger.Info("models migration completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// getModelName возвращает имя модели для логирования
|
||||
func getModelName(model interface{}) string {
|
||||
switch model.(type) {
|
||||
case *models.User:
|
||||
return "User"
|
||||
case *models.News:
|
||||
return "News"
|
||||
case *models.Comment:
|
||||
return "Comment"
|
||||
case *models.Review:
|
||||
return "Reviews"
|
||||
case *models.UserStats:
|
||||
return "Статистика Пользователя"
|
||||
case *models.Workout:
|
||||
return "Тренировки пользователя"
|
||||
case *models.Achievement:
|
||||
return "Достижения пользователя"
|
||||
case *models.Event:
|
||||
return "Событие"
|
||||
case *models.EventRegistration:
|
||||
return "Администрирование события"
|
||||
case *models.PersonalBest:
|
||||
return "Персональные достижения"
|
||||
case *models.TrainingPlan:
|
||||
return "Тренировочный план"
|
||||
case *models.EmailVerification:
|
||||
return "Верификация email"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// handlers/auth.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
authService service.AuthService
|
||||
jwtService service.JWTService
|
||||
logger logger.LoggerInterface
|
||||
emailService service.EmailService
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService service.AuthService, jwtService service.JWTService, emailService service.EmailService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
jwtService: jwtService,
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "auth"))),
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
h.logger.Info("handling register request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Логируем тело запроса для отладки
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to read request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Восстанавливаем тело для дальнейшего использования
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
h.logger.Debug("raw register request body", zap.String("body", string(bodyBytes)))
|
||||
|
||||
var req RegisterRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid JSON payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("parsed register request",
|
||||
zap.String("email", req.Email),
|
||||
zap.String("first_name", req.FirstName),
|
||||
zap.String("last_name", req.LastName),
|
||||
)
|
||||
|
||||
// Валидация обязательных полей
|
||||
if req.FirstName == "" {
|
||||
h.logger.Warn("register failed - first name required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "First name is required")
|
||||
return
|
||||
}
|
||||
if req.LastName == "" {
|
||||
h.logger.Warn("register failed - last name required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Last name is required")
|
||||
return
|
||||
}
|
||||
if req.Email == "" {
|
||||
h.logger.Warn("register failed - email required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Email is required")
|
||||
return
|
||||
}
|
||||
if req.Password == "" {
|
||||
h.logger.Warn("register failed - password required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Password is required")
|
||||
return
|
||||
}
|
||||
if len(req.Password) < 6 {
|
||||
h.logger.Warn("register failed - password too short")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Password must be at least 6 characters")
|
||||
return
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Phone: req.Phone,
|
||||
Experience: req.Experience,
|
||||
Goals: req.Goals,
|
||||
Newsletter: req.Newsletter,
|
||||
Role: "user",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.authService.Register(user); err != nil {
|
||||
h.logger.Error("auth service registration failed",
|
||||
zap.String("email", req.Email),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("user registered successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", user.Email),
|
||||
)
|
||||
|
||||
// Отправки сообщения для верификации Email
|
||||
if err := h.emailService.SendVerificationEmail(user.ID, user.Email, user.FirstName); err != nil {
|
||||
h.logger.Error("failed to send verification email",
|
||||
zap.Error(err),
|
||||
zap.Uint("user_id", user.ID))
|
||||
}
|
||||
|
||||
// После успешной регистрации возвращаем данные пользователя
|
||||
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "User registered successfully",
|
||||
"user": toUserResponse(user),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling login request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем Content-Type
|
||||
if r.Header.Get("Content-Type") != "application/json" {
|
||||
h.logger.Warn("invalid content type", zap.String("content_type", r.Header.Get("Content-Type")))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Content-Type must be application/json")
|
||||
return
|
||||
}
|
||||
|
||||
// Читаем и логируем тело запроса
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to read request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body")
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Восстанавливаем тело
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
h.logger.Debug("request body", zap.String("body", string(bodyBytes)))
|
||||
|
||||
var req LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("JSON decode failed",
|
||||
zap.Error(err),
|
||||
zap.String("raw_body", string(bodyBytes)),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid JSON: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
req.Email = strings.TrimSpace(req.Email)
|
||||
req.Password = strings.TrimSpace(req.Password)
|
||||
|
||||
// Валидация
|
||||
if req.Email == "" || req.Password == "" {
|
||||
h.logger.Warn("validation failed",
|
||||
zap.String("email", req.Email),
|
||||
zap.Int("password_len", len(req.Password)),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Email and password are required")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("attempting login", zap.String("email", req.Email))
|
||||
|
||||
user, token, err := h.authService.Login(req.Email, req.Password)
|
||||
if err != nil {
|
||||
h.logger.Warn("login failed", zap.String("email", req.Email), zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем куки
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
})
|
||||
|
||||
h.logger.Info("login successful",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", user.Email),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Login successful",
|
||||
"token": token,
|
||||
"user": toUserResponse(user),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
// Устанавливаем CORS заголовки
|
||||
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
|
||||
h.logger.Info("handling logout request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Удаляем куку
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Expires: time.Now().Add(-1 * time.Hour),
|
||||
MaxAge: -1,
|
||||
})
|
||||
|
||||
h.logger.Info("user logged out successfully")
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Logout successful",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// handlers/avatar.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AvatarHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
avatarService service.AvatarService
|
||||
}
|
||||
|
||||
func NewAvatarHandler(avatarService service.AvatarService) *AvatarHandler {
|
||||
return &AvatarHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "avatar"))),
|
||||
avatarService: avatarService,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AvatarHandler) UploadAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("UploadAvatar START",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
defer func() {
|
||||
h.logger.Debug("UploadAvatar END",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
)
|
||||
}()
|
||||
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("UploadAvatar: authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("UploadAvatar: user authenticated",
|
||||
zap.Int64("user_id", int64(user.ID)),
|
||||
zap.String("username", user.FirstName+user.LastName),
|
||||
)
|
||||
|
||||
// Парсим multipart форму
|
||||
h.logger.Debug("UploadAvatar: parsing multipart form")
|
||||
if err := r.ParseMultipartForm(10 << 20); err != nil { // 10MB limit
|
||||
h.logger.Error("UploadAvatar: failed to parse form", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Failed to parse form: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("UploadAvatar: getting file from form")
|
||||
file, header, err := r.FormFile("avatar")
|
||||
if err != nil {
|
||||
h.logger.Error("UploadAvatar: failed to get file from form", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Failed to get file: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
h.logger.Debug("UploadAvatar: file received",
|
||||
zap.String("filename", header.Filename),
|
||||
zap.Int64("size", header.Size),
|
||||
zap.String("content_type", header.Header.Get("Content-Type")),
|
||||
)
|
||||
|
||||
// Проверяем тип файла
|
||||
allowedTypes := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/jpg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
}
|
||||
contentType := header.Header.Get("Content-Type")
|
||||
if !allowedTypes[contentType] {
|
||||
h.logger.Warn("UploadAvatar: invalid file type",
|
||||
zap.String("content_type", contentType),
|
||||
zap.String("filename", header.Filename),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Only JPEG, PNG and GIF images are allowed")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("UploadAvatar: file type validated successfully")
|
||||
|
||||
// Загружаем аватар
|
||||
h.logger.Debug("UploadAvatar: calling avatarService.UploadAvatar",
|
||||
zap.Int64("user_id", int64(user.ID)),
|
||||
)
|
||||
avatarPath, err := h.avatarService.UploadAvatar(user.ID, file, header)
|
||||
if err != nil {
|
||||
h.logger.Error("UploadAvatar: failed to upload avatar", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to upload avatar: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("UploadAvatar: avatar uploaded successfully",
|
||||
zap.Int64("user_id", int64(user.ID)),
|
||||
zap.String("avatar_path", avatarPath),
|
||||
)
|
||||
|
||||
// Возвращаем ответ с полем success
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Avatar uploaded successfully",
|
||||
"avatar": avatarPath,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AvatarHandler) DeleteAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("DeleteAvatar START",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
defer func() {
|
||||
h.logger.Debug("DeleteAvatar END",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
)
|
||||
}()
|
||||
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("DeleteAvatar: authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("DeleteAvatar: user authenticated",
|
||||
zap.Int64("user_id", int64(user.ID)),
|
||||
zap.String("username", user.FirstName+user.LastName),
|
||||
)
|
||||
|
||||
h.logger.Debug("DeleteAvatar: calling avatarService.DeleteAvatar",
|
||||
zap.Int64("user_id", int64(user.ID)),
|
||||
)
|
||||
if err := h.avatarService.DeleteAvatar(user.ID); err != nil {
|
||||
h.logger.Error("DeleteAvatar: failed to delete avatar", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete avatar: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("DeleteAvatar: avatar deleted successfully",
|
||||
zap.Int64("user_id", int64(user.ID)),
|
||||
)
|
||||
|
||||
// Возвращаем ответ с полем success
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Avatar deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GET /v1/user/avatars/{filename}
|
||||
func (h *AvatarHandler) GetAvatar(w http.ResponseWriter, r *http.Request) {
|
||||
filename := chi.URLParam(r, "filename")
|
||||
|
||||
h.logger.Debug("GetAvatar START",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("filename", filename),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
zap.String("url", r.URL.String()),
|
||||
)
|
||||
|
||||
defer func() {
|
||||
h.logger.Debug("GetAvatar END",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("filename", filename),
|
||||
)
|
||||
}()
|
||||
|
||||
// Валидация имени файла
|
||||
if filename == "" || strings.Contains(filename, "..") || strings.Contains(filename, "/") {
|
||||
h.logger.Warn("GetAvatar: invalid filename", zap.String("filename", filename))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid filename")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("GetAvatar: handling get avatar request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("filename", filename),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Используем ServeAvatarFile для обслуживания файла
|
||||
h.logger.Debug("GetAvatar: calling avatarService.ServeAvatarFile",
|
||||
zap.String("filename", filename),
|
||||
)
|
||||
contentType, err := h.avatarService.ServeAvatarFile(w, filename)
|
||||
if err != nil {
|
||||
h.logger.Warn("GetAvatar: failed to serve avatar file",
|
||||
zap.String("filename", filename),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
switch {
|
||||
case err.Error() == "avatar file not found":
|
||||
h.logger.Warn("GetAvatar: avatar file not found", zap.String("filename", filename))
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Avatar not found")
|
||||
case err.Error() == "invalid filename" || err.Error() == "unsupported file format":
|
||||
h.logger.Warn("GetAvatar: invalid filename or format",
|
||||
zap.String("filename", filename),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, err.Error())
|
||||
default:
|
||||
h.logger.Error("GetAvatar: internal server error", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to serve avatar")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем заголовки для кэширования
|
||||
h.logger.Debug("GetAvatar: setting response headers",
|
||||
zap.String("content_type", contentType),
|
||||
)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000") // Кэш на 1 год
|
||||
w.Header().Set("Expires", time.Now().Add(365*24*time.Hour).Format(http.TimeFormat))
|
||||
|
||||
h.logger.Info("GetAvatar: avatar served successfully",
|
||||
zap.String("filename", filename),
|
||||
zap.String("content_type", contentType),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// handlers/email_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type EmailHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
emailService *service.EmailService
|
||||
}
|
||||
|
||||
func NewEmailHandler(emailService *service.EmailService) *EmailHandler {
|
||||
return &EmailHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "email"))),
|
||||
emailService: emailService,
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyEmail подтверждает email пользователя
|
||||
func (h *EmailHandler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling email verification request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
h.logger.Warn("email verification failed - token is required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Токен обязателен")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.emailService.VerifyEmail(token); err != nil {
|
||||
h.logger.Error("email verification failed, expired",
|
||||
zap.Error(err),
|
||||
zap.String("token", token),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Неверный или просроченный токен")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("email successfully verified",
|
||||
zap.String("token", token),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Email успешно подтвержден",
|
||||
})
|
||||
}
|
||||
|
||||
// RequestPasswordReset запрашивает сброс пароля
|
||||
func (h *EmailHandler) RequestPasswordReset(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling password reset request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
var req models.PasswordResetRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Warn("password reset request failed - invalid request format",
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Неверный формат запроса")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.emailService.SendPasswordResetEmail(req.Email); err != nil {
|
||||
h.logger.Error("password reset request failed",
|
||||
zap.Error(err),
|
||||
zap.String("email", req.Email),
|
||||
)
|
||||
// Для безопасности всегда возвращаем успех
|
||||
}
|
||||
|
||||
h.logger.Info("password reset request processed",
|
||||
zap.String("email", req.Email),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Если email зарегистрирован, инструкции по восстановлению пароля будут отправлены",
|
||||
})
|
||||
}
|
||||
|
||||
// ConfirmPasswordReset подтверждает сброс пароля
|
||||
func (h *EmailHandler) ConfirmPasswordReset(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling password reset confirmation request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
var req models.PasswordResetConfirm
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Warn("password reset confirmation failed - invalid request format",
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Неверный формат запроса")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.emailService.ResetPassword(req.Token, req.Password); err != nil {
|
||||
h.logger.Error("password reset confirmation failed",
|
||||
zap.Error(err),
|
||||
zap.String("token", req.Token),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Неверный или просроченный токен")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("password successfully reset",
|
||||
zap.String("token", req.Token),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Пароль успешно изменен",
|
||||
})
|
||||
}
|
||||
|
||||
type NewsletterRequest struct {
|
||||
Subject string `json:"subject" validate:"required"`
|
||||
Content string `json:"content" validate:"required"`
|
||||
}
|
||||
|
||||
// SendNewsletter отправляет рассылку новостей
|
||||
func (h *EmailHandler) SendNewsletter(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling newsletter sending request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
var req NewsletterRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Warn("newsletter sending failed - invalid request format",
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Неверный формат запроса")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.emailService.SendNewsletterToSubscribers(req.Subject, req.Content); err != nil {
|
||||
h.logger.Error("newsletter sending failed",
|
||||
zap.Error(err),
|
||||
zap.String("subject", req.Subject),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Не удалось отправить рассылку")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("newsletter sent successfully",
|
||||
zap.String("subject", req.Subject),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Рассылка отправлена подписчикам",
|
||||
})
|
||||
}
|
||||
|
||||
// ResendVerification повторно отправляет email верификации
|
||||
func (h *EmailHandler) ResendVerification(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling resend verification request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("resend verification failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Пользователь не авторизован")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем пользователя
|
||||
userData, err := h.emailService.GetUserByID(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Warn("resend verification failed - user not found",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Пользователь не найден")
|
||||
return
|
||||
}
|
||||
|
||||
if userData.EmailVerified {
|
||||
h.logger.Warn("resend verification failed - email already verified",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", userData.Email),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Email уже подтвержден")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.emailService.SendVerificationEmail(userData.ID, userData.Email, userData.FirstName); err != nil {
|
||||
h.logger.Error("resend verification failed",
|
||||
zap.Error(err),
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", userData.Email),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Не удалось отправить email подтверждения")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("verification email resent successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", userData.Email),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Email подтверждения отправлен повторно",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
// handlers/event_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type EventHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
eventService service.EventService
|
||||
}
|
||||
|
||||
func NewEventHandler(eventService service.EventService) *EventHandler {
|
||||
return &EventHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "event"))),
|
||||
eventService: eventService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateEventRequest - DTO для создания события
|
||||
type CreateEventRequest struct {
|
||||
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||
Description string `json:"description" validate:"required,min=10"`
|
||||
Date time.Time `json:"date" validate:"required"`
|
||||
Location string `json:"location" validate:"required,max=255"`
|
||||
Type models.EventType `json:"type" validate:"required,oneof=race training social workshop"`
|
||||
Distance string `json:"distance" validate:"max=50"`
|
||||
MaxParticipants int `json:"max_participants" validate:"min=0"`
|
||||
RegistrationOpen bool `json:"registration_open"`
|
||||
Image string `json:"image" validate:"max=500"`
|
||||
}
|
||||
|
||||
// UpdateEventRequest - DTO для обновления события
|
||||
type UpdateEventRequest struct {
|
||||
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||
Description string `json:"description" validate:"required,min=10"`
|
||||
Date time.Time `json:"date" validate:"required"`
|
||||
Location string `json:"location" validate:"required,max=255"`
|
||||
Type models.EventType `json:"type" validate:"required,oneof=race training social workshop"`
|
||||
Distance string `json:"distance" validate:"max=50"`
|
||||
MaxParticipants int `json:"max_participants" validate:"min=0"`
|
||||
RegistrationOpen bool `json:"registration_open"`
|
||||
Image string `json:"image" validate:"max=500"`
|
||||
}
|
||||
|
||||
// EventResponse - DTO для ответа с событием
|
||||
type EventResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Date time.Time `json:"date"`
|
||||
Location string `json:"location"`
|
||||
Type models.EventType `json:"type"`
|
||||
Distance string `json:"distance"`
|
||||
ParticipantsCount int `json:"participants_count"`
|
||||
MaxParticipants int `json:"max_participants"`
|
||||
RegistrationOpen bool `json:"registration_open"`
|
||||
Image string `json:"image"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateEvent создает новое событие
|
||||
func (h *EventHandler) CreateEvent(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling create event request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("create event failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем права доступа (только админы могут создавать события)
|
||||
if user.Role != "admin" {
|
||||
h.logger.Warn("create event failed - insufficient permissions",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("user_role", user.Role),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateEventRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Error("failed to decode request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("validation failed for create event", zap.Error(err))
|
||||
utils.RespondWithValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем модель события
|
||||
event := &models.Event{
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Date: req.Date,
|
||||
Location: req.Location,
|
||||
Type: req.Type,
|
||||
Distance: req.Distance,
|
||||
MaxParticipants: req.MaxParticipants,
|
||||
RegistrationOpen: req.RegistrationOpen,
|
||||
Image: req.Image,
|
||||
}
|
||||
|
||||
if err := h.eventService.CreateEvent(event); err != nil {
|
||||
h.logger.Error("failed to create event", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create event: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("event created successfully",
|
||||
zap.Uint("event_id", event.ID),
|
||||
zap.String("title", event.Title),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "Event created successfully",
|
||||
"event": toEventResponse(event),
|
||||
})
|
||||
}
|
||||
|
||||
// GetEvent возвращает событие по ID
|
||||
func (h *EventHandler) GetEvent(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get event request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Извлекаем ID события из URL параметров
|
||||
eventID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid event ID", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
|
||||
return
|
||||
}
|
||||
|
||||
event, err := h.eventService.GetEventByID(uint(eventID))
|
||||
if err != nil {
|
||||
h.logger.Warn("event not found",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Event not found")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("event retrieved successfully",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.String("title", event.Title),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, toEventResponse(event))
|
||||
}
|
||||
|
||||
// GetAllEvents возвращает все события
|
||||
func (h *EventHandler) GetAllEvents(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get all events request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
events, err := h.eventService.GetAllEvents()
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get events", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get events: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Преобразуем в response формат
|
||||
var eventResponses []EventResponse
|
||||
for _, event := range events {
|
||||
eventResponses = append(eventResponses, toEventResponse(&event))
|
||||
}
|
||||
|
||||
h.logger.Info("events list retrieved successfully",
|
||||
zap.Int("events_count", len(eventResponses)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, eventResponses)
|
||||
}
|
||||
|
||||
// UpdateEvent обновляет событие
|
||||
func (h *EventHandler) UpdateEvent(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling update event request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию и права
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update event failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Role != "admin" {
|
||||
h.logger.Warn("update event failed - insufficient permissions",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("user_role", user.Role),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID события
|
||||
eventID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid event ID", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateEventRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Error("failed to decode request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("validation failed for update event", zap.Error(err))
|
||||
utils.RespondWithValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем модель события для обновления
|
||||
event := &models.Event{
|
||||
ID: uint(eventID),
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Date: req.Date,
|
||||
Location: req.Location,
|
||||
Type: req.Type,
|
||||
Distance: req.Distance,
|
||||
MaxParticipants: req.MaxParticipants,
|
||||
RegistrationOpen: req.RegistrationOpen,
|
||||
Image: req.Image,
|
||||
}
|
||||
|
||||
if err := h.eventService.UpdateEvent(event); err != nil {
|
||||
h.logger.Error("failed to update event",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update event: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("event updated successfully",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.String("title", event.Title),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Event updated successfully",
|
||||
"event": toEventResponse(event),
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteEvent удаляет событие
|
||||
func (h *EventHandler) DeleteEvent(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling delete event request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию и права
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("delete event failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Role != "admin" {
|
||||
h.logger.Warn("delete event failed - insufficient permissions",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("user_role", user.Role),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID события
|
||||
eventID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid event ID", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.eventService.DeleteEvent(uint(eventID)); err != nil {
|
||||
h.logger.Error("failed to delete event",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete event: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("event deleted successfully",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Event deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetEventsByType возвращает события по типу
|
||||
func (h *EventHandler) GetEventsByType(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get events by type request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
eventType := models.EventType(r.PathValue("type"))
|
||||
|
||||
// Валидация типа события
|
||||
validTypes := []models.EventType{"race", "training", "social", "workshop"}
|
||||
if !isValidEventType(eventType, validTypes) {
|
||||
h.logger.Warn("invalid event type", zap.String("event_type", string(eventType)))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event type")
|
||||
return
|
||||
}
|
||||
|
||||
events, err := h.eventService.GetEventsByType(eventType)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get events by type",
|
||||
zap.String("event_type", string(eventType)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get events: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var eventResponses []EventResponse
|
||||
for _, event := range events {
|
||||
eventResponses = append(eventResponses, toEventResponse(&event))
|
||||
}
|
||||
|
||||
h.logger.Info("events by type retrieved successfully",
|
||||
zap.String("event_type", string(eventType)),
|
||||
zap.Int("events_count", len(eventResponses)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, eventResponses)
|
||||
}
|
||||
|
||||
// GetUpcomingEvents возвращает предстоящие события
|
||||
func (h *EventHandler) GetUpcomingEvents(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get upcoming events request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
events, err := h.eventService.GetUpcomingEvents()
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get upcoming events", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get upcoming events: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var eventResponses []EventResponse
|
||||
for _, event := range events {
|
||||
eventResponses = append(eventResponses, toEventResponse(&event))
|
||||
}
|
||||
|
||||
h.logger.Info("upcoming events retrieved successfully",
|
||||
zap.Int("events_count", len(eventResponses)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, eventResponses)
|
||||
}
|
||||
|
||||
// ToggleRegistrationStatus переключает статус регистрации
|
||||
func (h *EventHandler) ToggleRegistrationStatus(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling toggle registration status request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию и права
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("toggle registration status failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Role != "admin" {
|
||||
h.logger.Warn("toggle registration status failed - insufficient permissions",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("user_role", user.Role),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID события
|
||||
eventID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid event ID", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
RegistrationOpen bool `json:"registration_open" validate:"required"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.eventService.ToggleRegistrationStatus(uint(eventID), req.RegistrationOpen); err != nil {
|
||||
h.logger.Error("failed to toggle registration status",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.Bool("registration_open", req.RegistrationOpen),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to toggle registration status: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("registration status toggled successfully",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.Bool("registration_open", req.RegistrationOpen),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Registration status updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// toEventResponse преобразует модель события в response DTO
|
||||
func toEventResponse(event *models.Event) EventResponse {
|
||||
return EventResponse{
|
||||
ID: event.ID,
|
||||
Title: event.Title,
|
||||
Description: event.Description,
|
||||
Date: event.Date,
|
||||
Location: event.Location,
|
||||
Type: event.Type,
|
||||
Distance: event.Distance,
|
||||
ParticipantsCount: event.ParticipantsCount,
|
||||
MaxParticipants: event.MaxParticipants,
|
||||
RegistrationOpen: event.RegistrationOpen,
|
||||
Image: event.Image,
|
||||
CreatedAt: event.CreatedAt,
|
||||
UpdatedAt: event.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// isValidEventType проверяет валидность типа события
|
||||
func isValidEventType(eventType models.EventType, validTypes []models.EventType) bool {
|
||||
for _, validType := range validTypes {
|
||||
if eventType == validType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
// handlers/event_registration_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type EventRegistrationHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
registrationService service.EventRegistrationService
|
||||
}
|
||||
|
||||
func NewEventRegistrationHandler(registrationService service.EventRegistrationService) *EventRegistrationHandler {
|
||||
return &EventRegistrationHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "event_registration"))),
|
||||
registrationService: registrationService,
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterForEventRequest - DTO для регистрации на событие
|
||||
type RegisterForEventRequest struct {
|
||||
EventID uint `json:"event_id" validate:"required"`
|
||||
Notes string `json:"notes" validate:"max=500"`
|
||||
}
|
||||
|
||||
// UpdateRegistrationRequest - DTO для обновления регистрации
|
||||
type UpdateRegistrationRequest struct {
|
||||
Notes string `json:"notes" validate:"max=500"`
|
||||
}
|
||||
|
||||
// RegistrationResponse - DTO для ответа с регистрацией
|
||||
type RegistrationResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
EventID uint `json:"event_id"`
|
||||
Status string `json:"status"`
|
||||
Notes string `json:"notes"`
|
||||
ResultTime string `json:"result_time"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Event EventResponse `json:"event,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterForEvent регистрирует пользователя на событие
|
||||
func (h *EventRegistrationHandler) RegisterForEvent(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling register for event request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("register for event failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
var req RegisterForEventRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Error("failed to decode request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("validation failed for register for event", zap.Error(err))
|
||||
utils.RespondWithValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем модель регистрации
|
||||
registration := &models.EventRegistration{
|
||||
UserID: user.ID,
|
||||
EventID: req.EventID,
|
||||
Status: "pending",
|
||||
Notes: req.Notes,
|
||||
}
|
||||
|
||||
if err := h.registrationService.RegisterForEvent(registration); err != nil {
|
||||
h.logger.Error("failed to register for event",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("event_id", req.EventID),
|
||||
zap.Error(err),
|
||||
)
|
||||
statusCode := http.StatusInternalServerError
|
||||
if err.Error() == "event not found" {
|
||||
statusCode = http.StatusNotFound
|
||||
} else if err.Error() == "user already registered for this event" {
|
||||
statusCode = http.StatusConflict
|
||||
} else if err.Error() == "registration is closed for this event" {
|
||||
statusCode = http.StatusForbidden
|
||||
} else if err.Error() == "event is full" {
|
||||
statusCode = http.StatusConflict
|
||||
}
|
||||
utils.RespondWithError(w, statusCode, "Failed to register for event: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("user registered for event successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("event_id", req.EventID),
|
||||
zap.Uint("registration_id", registration.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "Successfully registered for event",
|
||||
"registration": toRegistrationResponse(registration),
|
||||
})
|
||||
}
|
||||
|
||||
// GetRegistration возвращает регистрацию по ID
|
||||
func (h *EventRegistrationHandler) GetRegistration(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get registration request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get registration failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID регистрации
|
||||
registrationID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid registration ID", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid registration ID")
|
||||
return
|
||||
}
|
||||
|
||||
registration, err := h.registrationService.GetRegistrationByID(uint(registrationID))
|
||||
if err != nil {
|
||||
h.logger.Warn("registration not found",
|
||||
zap.Uint("registration_id", uint(registrationID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Registration not found")
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем права доступа (пользователь может видеть только свои регистрации, админ - все)
|
||||
if user.Role != "admin" && registration.UserID != user.ID {
|
||||
h.logger.Warn("access denied to registration",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("registration_user_id", registration.UserID),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("registration retrieved successfully",
|
||||
zap.Uint("registration_id", uint(registrationID)),
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, toRegistrationResponse(registration))
|
||||
}
|
||||
|
||||
// GetUserRegistrations возвращает все регистрации пользователя
|
||||
func (h *EventRegistrationHandler) GetUserRegistrations(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get user registrations request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get user registrations failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
registrations, err := h.registrationService.GetRegistrationsByUserID(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get user registrations",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get registrations: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var registrationResponses []RegistrationResponse
|
||||
for _, registration := range registrations {
|
||||
registrationResponses = append(registrationResponses, toRegistrationResponse(®istration))
|
||||
}
|
||||
|
||||
h.logger.Info("user registrations retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("registrations_count", len(registrationResponses)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, registrationResponses)
|
||||
}
|
||||
|
||||
// GetEventRegistrations возвращает все регистрации на событие
|
||||
func (h *EventRegistrationHandler) GetEventRegistrations(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get event registrations request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию и права (только админы могут видеть все регистрации на событие)
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get event registrations failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Role != "admin" {
|
||||
h.logger.Warn("get event registrations failed - insufficient permissions",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("user_role", user.Role),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID события
|
||||
eventID, err := strconv.ParseUint(r.PathValue("eventId"), 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid event ID", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
|
||||
return
|
||||
}
|
||||
|
||||
registrations, err := h.registrationService.GetRegistrationsByEventID(uint(eventID))
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get event registrations",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get registrations: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var registrationResponses []RegistrationResponse
|
||||
for _, registration := range registrations {
|
||||
registrationResponses = append(registrationResponses, toRegistrationResponse(®istration))
|
||||
}
|
||||
|
||||
h.logger.Info("event registrations retrieved successfully",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.Int("registrations_count", len(registrationResponses)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, registrationResponses)
|
||||
}
|
||||
|
||||
// CancelRegistration отменяет регистрацию
|
||||
func (h *EventRegistrationHandler) CancelRegistration(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling cancel registration request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("cancel registration failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID регистрации
|
||||
registrationID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid registration ID", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid registration ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем права доступа
|
||||
registration, err := h.registrationService.GetRegistrationByID(uint(registrationID))
|
||||
if err != nil {
|
||||
h.logger.Warn("registration not found for cancellation",
|
||||
zap.Uint("registration_id", uint(registrationID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Registration not found")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Role != "admin" && registration.UserID != user.ID {
|
||||
h.logger.Warn("access denied to cancel registration",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("registration_user_id", registration.UserID),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.registrationService.CancelRegistration(uint(registrationID)); err != nil {
|
||||
h.logger.Error("failed to cancel registration",
|
||||
zap.Uint("registration_id", uint(registrationID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to cancel registration: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("registration cancelled successfully",
|
||||
zap.Uint("registration_id", uint(registrationID)),
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Registration cancelled successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRegistrationStatus обновляет статус регистрации
|
||||
func (h *EventRegistrationHandler) UpdateRegistrationStatus(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling update registration status request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию и права (только админы)
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update registration status failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Role != "admin" {
|
||||
h.logger.Warn("update registration status failed - insufficient permissions",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("user_role", user.Role),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID регистрации
|
||||
registrationID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid registration ID", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid registration ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status" validate:"required,oneof=pending confirmed cancelled completed"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
|
||||
return
|
||||
}
|
||||
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("validation failed for update registration status", zap.Error(err))
|
||||
utils.RespondWithValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.registrationService.UpdateRegistrationStatus(uint(registrationID), req.Status); err != nil {
|
||||
h.logger.Error("failed to update registration status",
|
||||
zap.Uint("registration_id", uint(registrationID)),
|
||||
zap.String("status", req.Status),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update registration status: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("registration status updated successfully",
|
||||
zap.Uint("registration_id", uint(registrationID)),
|
||||
zap.String("status", req.Status),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Registration status updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateResultTime обновляет результат забега
|
||||
func (h *EventRegistrationHandler) UpdateResultTime(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling update result time request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Проверяем аутентификацию и права (только админы)
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update result time failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Role != "admin" {
|
||||
h.logger.Warn("update result time failed - insufficient permissions",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("user_role", user.Role),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID регистрации
|
||||
registrationID, err := strconv.ParseUint(r.PathValue("id"), 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid registration ID", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid registration ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ResultTime string `json:"result_time" validate:"required,max=20"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload")
|
||||
return
|
||||
}
|
||||
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("validation failed for update result time", zap.Error(err))
|
||||
utils.RespondWithValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.registrationService.UpdateResultTime(uint(registrationID), req.ResultTime); err != nil {
|
||||
h.logger.Error("failed to update result time",
|
||||
zap.Uint("registration_id", uint(registrationID)),
|
||||
zap.String("result_time", req.ResultTime),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update result time: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("result time updated successfully",
|
||||
zap.Uint("registration_id", uint(registrationID)),
|
||||
zap.String("result_time", req.ResultTime),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Result time updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// CheckEventAvailability проверяет доступность мест на событии
|
||||
func (h *EventRegistrationHandler) CheckEventAvailability(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling check event availability request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Извлекаем ID события
|
||||
eventID, err := strconv.ParseUint(r.PathValue("eventId"), 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid event ID", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid event ID")
|
||||
return
|
||||
}
|
||||
|
||||
available, err := h.registrationService.CheckEventAvailability(uint(eventID))
|
||||
if err != nil {
|
||||
h.logger.Error("failed to check event availability",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to check availability: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("event availability checked successfully",
|
||||
zap.Uint("event_id", uint(eventID)),
|
||||
zap.Bool("available", available),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"event_id": eventID,
|
||||
"available": available,
|
||||
})
|
||||
}
|
||||
|
||||
// toRegistrationResponse преобразует модель регистрации в response DTO
|
||||
func toRegistrationResponse(registration *models.EventRegistration) RegistrationResponse {
|
||||
response := RegistrationResponse{
|
||||
ID: registration.ID,
|
||||
UserID: registration.UserID,
|
||||
EventID: registration.EventID,
|
||||
Status: registration.Status,
|
||||
Notes: registration.Notes,
|
||||
ResultTime: registration.ResultTime,
|
||||
CreatedAt: registration.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
UpdatedAt: registration.UpdatedAt.Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
// Включаем информацию о событии, если она загружена
|
||||
if registration.Event != nil {
|
||||
response.Event = toEventResponse(registration.Event)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
)
|
||||
|
||||
// Общая функция для преобразования User в UserResponse
|
||||
func toUserResponse(user *models.User) UserResponse {
|
||||
return UserResponse{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
Avatar: user.Avatar,
|
||||
Phone: user.Phone,
|
||||
Experience: user.Experience,
|
||||
Goals: user.Goals,
|
||||
Newsletter: user.Newsletter,
|
||||
Role: user.Role,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// handlers/handlers.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api_bb/internal/config"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/email"
|
||||
"api_bb/pkg/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
healthHandler *HealthHandler
|
||||
authHandler *AuthHandler
|
||||
userHandler *UserHandler
|
||||
avatarHandler *AvatarHandler
|
||||
newsHandler *NewsHandler
|
||||
reviewHandler *ReviewHandler
|
||||
userStatsHandler *UserStatsHandler
|
||||
userWorkoutHandler *UserWorkoutHandler
|
||||
userAchievementHandler *UserAchievementHandler
|
||||
eventHandler *EventHandler
|
||||
eventRegistrationHandler *EventRegistrationHandler
|
||||
personalBestHandler *PersonalBestHandler
|
||||
trainingPlanHandler *TrainingPlanHandler
|
||||
emailHandler *EmailHandler
|
||||
// Здесь будут добавлены другие обработчики
|
||||
// userHandler *UserHandler
|
||||
// eventHandler *EventHandler
|
||||
// reviewHandler *ReviewHandler
|
||||
}
|
||||
|
||||
func NewHandler(db *gorm.DB, cfg *config.Config) *Handler {
|
||||
// Инициализация репозиториев
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
newsRepo := repository.NewNewsRepository(db)
|
||||
commentRepo := repository.NewCommentRepository(db)
|
||||
reviewRepo := repository.NewReviewRepository(db)
|
||||
userStatsRepo := repository.NewUserStatsRepository(db)
|
||||
userWorkoutRepo := repository.NewWorkoutRepository(db)
|
||||
userAchievemenRepo := repository.NewAchievementRepository(db)
|
||||
eventRepo := repository.NewEventRepository(db)
|
||||
eventRegistrationRepo := repository.NewEventRegistrationRepository(db)
|
||||
personalBestRepo := repository.NewPersonalBestRepository(db)
|
||||
trainingPlanRepo := repository.NewTrainingPlanRepository(db)
|
||||
emailRepo := repository.NewEmailRepository(db)
|
||||
|
||||
// Initialize logger
|
||||
baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер
|
||||
|
||||
// getConfig
|
||||
emailSender, err := email.NewService(config.Load())
|
||||
if err != nil {
|
||||
baseLogger.Info("error to load config", zap.Error(err))
|
||||
}
|
||||
|
||||
// Инициализация сервисов
|
||||
jwtService := service.NewJWTService(cfg.JWTSecret)
|
||||
authService := service.NewAuthService(userRepo, jwtService, baseLogger)
|
||||
userService := service.NewUserService(userRepo, jwtService, baseLogger)
|
||||
avatarService := service.NewAvatarService(userRepo, baseLogger)
|
||||
newsService := service.NewNewsService(newsRepo, commentRepo, baseLogger)
|
||||
reviewService := service.NewReviewService(reviewRepo, baseLogger)
|
||||
userStatsService := service.NewUserStatsService(userStatsRepo)
|
||||
userWorkoutService := service.NewWorkoutService(userWorkoutRepo)
|
||||
achievementService := service.NewAchievementService(userAchievemenRepo)
|
||||
eventRegistrationService := service.NewEventRegistrationService(eventRegistrationRepo, eventRepo, baseLogger)
|
||||
eventService := service.NewEventService(eventRepo, eventRegistrationRepo, baseLogger)
|
||||
personalBestService := service.NewPersonalBestService(personalBestRepo, userStatsService)
|
||||
trainingPlanService := service.NewTrainingPlanService(*trainingPlanRepo)
|
||||
emailService := service.NewEmailService(*emailRepo, userRepo, *emailSender)
|
||||
|
||||
// Инициализация обработчиков
|
||||
healthHandler := NewHealthHandler()
|
||||
authHandler := NewAuthHandler(authService, jwtService, emailService)
|
||||
userHandler := NewUserHandler(&userService)
|
||||
newsHandler := NewNewsHandler(newsService, baseLogger)
|
||||
avatarHandler := NewAvatarHandler(avatarService)
|
||||
reviewHandler := NewReviewHandler(reviewService, baseLogger)
|
||||
userStatsHandler := NewUserStatsHandler(userStatsService)
|
||||
userWorkoutHandler := NewUserWorkoutHandler(userWorkoutService)
|
||||
userAchievementHandler := NewUserAchievementHandler(*achievementService)
|
||||
eventHandler := NewEventHandler(eventService)
|
||||
eventRegistrationHandler := NewEventRegistrationHandler(eventRegistrationService)
|
||||
personalBestHandler := NewPersonalBestHandler(*personalBestService)
|
||||
trainingPlanHandler := NewTrainingPlanHandler(trainingPlanService)
|
||||
emailHandler := NewEmailHandler(&emailService)
|
||||
|
||||
return &Handler{
|
||||
healthHandler: healthHandler,
|
||||
authHandler: authHandler,
|
||||
userHandler: userHandler,
|
||||
newsHandler: newsHandler,
|
||||
avatarHandler: avatarHandler,
|
||||
reviewHandler: reviewHandler,
|
||||
userStatsHandler: userStatsHandler,
|
||||
userWorkoutHandler: userWorkoutHandler,
|
||||
userAchievementHandler: userAchievementHandler,
|
||||
eventHandler: eventHandler,
|
||||
eventRegistrationHandler: eventRegistrationHandler,
|
||||
personalBestHandler: personalBestHandler,
|
||||
trainingPlanHandler: trainingPlanHandler,
|
||||
emailHandler: emailHandler,
|
||||
}
|
||||
}
|
||||
|
||||
// Геттеры для обработчиков (опционально, для удобства)
|
||||
func (h *Handler) EmailHandler() *EmailHandler {
|
||||
return h.emailHandler
|
||||
}
|
||||
|
||||
func (h *Handler) TrainingPlanHandler() *TrainingPlanHandler {
|
||||
return h.trainingPlanHandler
|
||||
}
|
||||
|
||||
func (h *Handler) PersonalBestHandler() *PersonalBestHandler {
|
||||
return h.personalBestHandler
|
||||
}
|
||||
|
||||
func (h *Handler) EventHandler() *EventHandler {
|
||||
return h.eventHandler
|
||||
}
|
||||
|
||||
func (h *Handler) EventRegistrationHandler() *EventRegistrationHandler {
|
||||
return h.eventRegistrationHandler
|
||||
}
|
||||
|
||||
func (h *Handler) HealthHandler() *HealthHandler {
|
||||
return h.healthHandler
|
||||
}
|
||||
|
||||
func (h *Handler) AuthHandler() *AuthHandler {
|
||||
return h.authHandler
|
||||
}
|
||||
|
||||
func (h *Handler) UserHandler() *UserHandler {
|
||||
return h.userHandler
|
||||
}
|
||||
|
||||
func (h *Handler) AvatarHandler() *AvatarHandler {
|
||||
return h.avatarHandler
|
||||
}
|
||||
|
||||
func (h *Handler) NewsHandler() *NewsHandler {
|
||||
return h.newsHandler
|
||||
}
|
||||
|
||||
func (h *Handler) ReviewHandler() *ReviewHandler {
|
||||
return h.reviewHandler
|
||||
}
|
||||
|
||||
func (h *Handler) UserStatsHandler() *UserStatsHandler {
|
||||
return h.userStatsHandler
|
||||
}
|
||||
|
||||
func (h *Handler) UserWorkoutHandler() *UserWorkoutHandler {
|
||||
return h.userWorkoutHandler
|
||||
}
|
||||
|
||||
func (h *Handler) UserAchievementHandler() *UserAchievementHandler {
|
||||
return h.userAchievementHandler
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
)
|
||||
|
||||
type HealthHandler struct{}
|
||||
|
||||
func NewHealthHandler() *HealthHandler {
|
||||
return &HealthHandler{}
|
||||
}
|
||||
|
||||
func (h *HealthHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]string{
|
||||
"status": "ok",
|
||||
"message": "Service is healthy",
|
||||
}
|
||||
utils.RespondWithJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Check(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]string{
|
||||
"status": "ok",
|
||||
"message": "API is working",
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, response)
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type NewsHandler struct {
|
||||
newsService service.NewsService
|
||||
logger logger.LoggerInterface
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
func NewNewsHandler(newsService service.NewsService, log logger.LoggerInterface) *NewsHandler {
|
||||
return &NewsHandler{
|
||||
newsService: newsService,
|
||||
logger: log,
|
||||
validator: validator.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetNews возвращает список новостей с пагинацией и фильтрацией
|
||||
func (h *NewsHandler) GetNews(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("Start GetNews Method")
|
||||
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
category := r.URL.Query().Get("category")
|
||||
|
||||
h.logger.Debug("GetNews parameters",
|
||||
zap.Int("limit", limit),
|
||||
zap.Int("offset", offset),
|
||||
zap.String("category", category),
|
||||
)
|
||||
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
if limit > 50 {
|
||||
limit = 50
|
||||
}
|
||||
|
||||
news, total, err := h.newsService.GetAllNews(limit, offset, category)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get news")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Successfully retrieved news",
|
||||
zap.Int("count", len(news)),
|
||||
zap.Int("total", int(total)),
|
||||
)
|
||||
|
||||
h.logger.Debug("End GetNews Method")
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"news": news,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
}
|
||||
|
||||
// GetNewsByID возвращает конкретную новость
|
||||
func (h *NewsHandler) GetNewsByID(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("Start GetNewsByID Method")
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
h.logger.Debug("GetNewsByID parameters", zap.String("id", idStr))
|
||||
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("Invalid news ID", zap.String("id", idStr), zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.GetNewsByID(uint(id))
|
||||
if err != nil {
|
||||
h.logger.Warn("News not found", zap.Uint("id", uint(id)), zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusNotFound, "News not found")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Successfully retrieved news by ID", zap.Uint("id", uint(id)))
|
||||
h.logger.Debug("End GetNewsByID Method")
|
||||
utils.RespondWithJSON(w, http.StatusOK, news)
|
||||
}
|
||||
|
||||
// CreateNews создает новую новость
|
||||
func (h *NewsHandler) CreateNews(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("Start CreateNews Method")
|
||||
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
h.logger.Debug("CreateNews user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
|
||||
|
||||
if !ok {
|
||||
h.logger.Warn("Failed to get userID from context in CreateNews",
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("method", r.Method),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateNewsRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Warn("Invalid request body in CreateNews", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("CreateNews request data",
|
||||
zap.String("title", req.Title),
|
||||
zap.String("category", string(req.Category)),
|
||||
)
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
h.logger.Warn("Validation failed in CreateNews", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.CreateNews(req, userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create news")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Successfully created news",
|
||||
zap.Uint("newsID", news.ID),
|
||||
zap.Uint("userID", userID),
|
||||
)
|
||||
h.logger.Debug("End CreateNews Method")
|
||||
utils.RespondWithJSON(w, http.StatusCreated, news)
|
||||
}
|
||||
|
||||
// UpdateNews обновляет новость
|
||||
func (h *NewsHandler) UpdateNews(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("Start UpdateNews Method")
|
||||
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
h.logger.Debug("UpdateNews user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
|
||||
|
||||
if !ok {
|
||||
h.logger.Warn("Failed to get userID from context in UpdateNews")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
h.logger.Debug("UpdateNews parameters", zap.String("id", idStr))
|
||||
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("Invalid news ID in UpdateNews", zap.String("id", idStr), zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdateNewsRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Warn("Invalid request body in UpdateNews", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("UpdateNews request data",
|
||||
zap.String("title", req.Title),
|
||||
zap.String("category", string(req.Category)),
|
||||
)
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
h.logger.Warn("Validation failed in UpdateNews", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
news, err := h.newsService.UpdateNews(uint(id), req, userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
h.logger.Warn("Access denied in UpdateNews",
|
||||
zap.Uint("userID", userID),
|
||||
zap.Uint("newsID", uint(id)),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to update news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update news")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Successfully updated news",
|
||||
zap.Uint("newsID", uint(id)),
|
||||
zap.Uint("userID", userID),
|
||||
)
|
||||
h.logger.Debug("End UpdateNews Method")
|
||||
utils.RespondWithJSON(w, http.StatusOK, news)
|
||||
}
|
||||
|
||||
// DeleteNews удаляет новость
|
||||
func (h *NewsHandler) DeleteNews(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("Start DeleteNews Method")
|
||||
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
h.logger.Debug("DeleteNews user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
|
||||
|
||||
if !ok {
|
||||
h.logger.Warn("Failed to get userID from context in DeleteNews")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
h.logger.Debug("DeleteNews parameters", zap.String("id", idStr))
|
||||
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("Invalid news ID in DeleteNews", zap.String("id", idStr), zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.newsService.DeleteNews(uint(id), userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
h.logger.Warn("Access denied in DeleteNews",
|
||||
zap.Uint("userID", userID),
|
||||
zap.Uint("newsID", uint(id)),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to delete news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete news")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Successfully deleted news",
|
||||
zap.Uint("newsID", uint(id)),
|
||||
zap.Uint("userID", userID),
|
||||
)
|
||||
h.logger.Debug("End DeleteNews Method")
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "News deleted successfully"})
|
||||
}
|
||||
|
||||
// CreateComment создает комментарий к новости
|
||||
func (h *NewsHandler) CreateComment(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("Start CreateComment Method")
|
||||
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
h.logger.Debug("CreateComment user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
|
||||
|
||||
if !ok {
|
||||
h.logger.Warn("Failed to get userID from context in CreateComment")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
newsIDStr := chi.URLParam(r, "id")
|
||||
h.logger.Debug("CreateComment parameters", zap.String("newsID", newsIDStr))
|
||||
|
||||
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("Invalid news ID in CreateComment", zap.String("newsID", newsIDStr), zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateCommentRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Warn("Invalid request body in CreateComment", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("CreateComment request data",
|
||||
zap.String("content", req.Content),
|
||||
)
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
h.logger.Warn("Validation failed in CreateComment", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := h.newsService.CreateComment(uint(newsID), req, userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create comment", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create comment")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Successfully created comment",
|
||||
zap.Uint("commentID", comment.ID),
|
||||
zap.Uint("newsID", uint(newsID)),
|
||||
zap.Uint("userID", userID),
|
||||
)
|
||||
h.logger.Debug("End CreateComment Method")
|
||||
utils.RespondWithJSON(w, http.StatusCreated, comment)
|
||||
}
|
||||
|
||||
// GetComments возвращает комментарии к новости
|
||||
func (h *NewsHandler) GetComments(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("Start GetComments Method")
|
||||
|
||||
newsIDStr := chi.URLParam(r, "id")
|
||||
h.logger.Debug("GetComments parameters", zap.String("newsID", newsIDStr))
|
||||
|
||||
newsID, err := strconv.ParseUint(newsIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("Invalid news ID in GetComments", zap.String("newsID", newsIDStr), zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid news ID")
|
||||
return
|
||||
}
|
||||
|
||||
comments, err := h.newsService.GetCommentsByNewsID(uint(newsID))
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get comments", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get comments")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Successfully retrieved comments",
|
||||
zap.Uint("newsID", uint(newsID)),
|
||||
zap.Int("count", len(comments)),
|
||||
)
|
||||
h.logger.Debug("End GetComments Method")
|
||||
utils.RespondWithJSON(w, http.StatusOK, comments)
|
||||
}
|
||||
|
||||
// DeleteComment удаляет комментарий
|
||||
func (h *NewsHandler) DeleteComment(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("Start DeleteComment Method")
|
||||
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
h.logger.Debug("DeleteComment user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
|
||||
|
||||
if !ok {
|
||||
h.logger.Warn("Failed to get userID from context in DeleteComment")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
commentIDStr := chi.URLParam(r, "commentId")
|
||||
h.logger.Debug("DeleteComment parameters", zap.String("commentID", commentIDStr))
|
||||
|
||||
commentID, err := strconv.ParseUint(commentIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("Invalid comment ID in DeleteComment", zap.String("commentID", commentIDStr), zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid comment ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.newsService.DeleteComment(uint(commentID), userID)
|
||||
if err != nil {
|
||||
if err.Error() == "access denied" {
|
||||
h.logger.Warn("Access denied in DeleteComment",
|
||||
zap.Uint("userID", userID),
|
||||
zap.Uint("commentID", uint(commentID)),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Access denied")
|
||||
return
|
||||
}
|
||||
h.logger.Error("Failed to delete comment", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete comment")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Successfully deleted comment",
|
||||
zap.Uint("commentID", uint(commentID)),
|
||||
zap.Uint("userID", userID),
|
||||
)
|
||||
h.logger.Debug("End DeleteComment Method")
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Comment deleted successfully"})
|
||||
}
|
||||
|
||||
// GetUserNews возвращает новости конкретного пользователя
|
||||
func (h *NewsHandler) GetUserNews(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("Start GetUserNews Method")
|
||||
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
h.logger.Debug("GetUserNews user context", zap.Uint("userID", userID), zap.Bool("ok", ok))
|
||||
|
||||
if !ok {
|
||||
h.logger.Warn("Failed to get userID from context in GetUserNews")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
|
||||
|
||||
h.logger.Debug("GetUserNews parameters",
|
||||
zap.Int("limit", limit),
|
||||
zap.Int("offset", offset),
|
||||
)
|
||||
|
||||
if limit == 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
news, total, err := h.newsService.GetUserNews(userID, limit, offset)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get user news", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user news")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Successfully retrieved user news",
|
||||
zap.Uint("userID", userID),
|
||||
zap.Int("count", len(news)),
|
||||
zap.Int("total", int(total)),
|
||||
)
|
||||
h.logger.Debug("End GetUserNews Method")
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"news": news,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
// handlers/personal_best_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type PersonalBestHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
personalBestService service.PersonalBestService
|
||||
}
|
||||
|
||||
func NewPersonalBestHandler(personalBestService service.PersonalBestService) *PersonalBestHandler {
|
||||
return &PersonalBestHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "personal_best"))),
|
||||
personalBestService: personalBestService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePersonalBest создает новый личный рекорд
|
||||
func (h *PersonalBestHandler) CreatePersonalBest(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling create personal best request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("create personal best failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.PersonalBestCreateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация
|
||||
if req.DistanceType == "" {
|
||||
h.logger.Warn("create personal best failed - distance type required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Distance type is required")
|
||||
return
|
||||
}
|
||||
if req.Time == "" {
|
||||
h.logger.Warn("create personal best failed - time required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Time is required")
|
||||
return
|
||||
}
|
||||
if req.Date.IsZero() {
|
||||
h.logger.Warn("create personal best failed - date required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Date is required")
|
||||
return
|
||||
}
|
||||
|
||||
personalBest, err := h.personalBestService.CreatePersonalBest(user.ID, req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to create personal best", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create personal best: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("personal best created successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("personal_best_id", personalBest.ID),
|
||||
zap.String("distance_type", string(personalBest.DistanceType)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, personalBest)
|
||||
}
|
||||
|
||||
// GetPersonalBest возвращает личный рекорд по ID
|
||||
func (h *PersonalBestHandler) GetPersonalBest(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get personal best request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid personal best ID", zap.String("id", idStr))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID")
|
||||
return
|
||||
}
|
||||
|
||||
personalBest, err := h.personalBestService.GetPersonalBestByID(uint(id))
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get personal best", zap.Error(err))
|
||||
if err.Error() == "record not found" {
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Personal best not found")
|
||||
} else {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal best: "+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("personal best retrieved successfully",
|
||||
zap.Uint("personal_best_id", personalBest.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, personalBest)
|
||||
}
|
||||
|
||||
// GetUserPersonalBests возвращает все личные рекорды пользователя
|
||||
func (h *PersonalBestHandler) GetUserPersonalBests(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get user personal bests request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get personal bests failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
personalBests, err := h.personalBestService.GetUserPersonalBests(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get personal bests", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal bests: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("user personal bests retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("count", len(personalBests)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, personalBests)
|
||||
}
|
||||
|
||||
// UpdatePersonalBest обновляет личный рекорд
|
||||
func (h *PersonalBestHandler) UpdatePersonalBest(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling update personal best request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update personal best failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid personal best ID", zap.String("id", idStr))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.PersonalBestUpdateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
personalBest, err := h.personalBestService.UpdatePersonalBest(uint(id), user.ID, req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to update personal best", zap.Error(err))
|
||||
if err.Error() == "record not found" {
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Personal best not found or access denied")
|
||||
} else {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update personal best: "+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("personal best updated successfully",
|
||||
zap.Uint("personal_best_id", personalBest.ID),
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, personalBest)
|
||||
}
|
||||
|
||||
// DeletePersonalBest удаляет личный рекорд
|
||||
func (h *PersonalBestHandler) DeletePersonalBest(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling delete personal best request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("delete personal best failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid personal best ID", zap.String("id", idStr))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.personalBestService.DeletePersonalBest(uint(id), user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to delete personal best", zap.Error(err))
|
||||
if err.Error() == "record not found" {
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Personal best not found or access denied")
|
||||
} else {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete personal best: "+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("personal best deleted successfully",
|
||||
zap.Uint("personal_best_id", uint(id)),
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Personal best deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetPersonalBestsByDistance возвращает личные рекорды по дистанции
|
||||
func (h *PersonalBestHandler) GetPersonalBestsByDistance(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get personal bests by distance request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get personal bests by distance failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
distanceType := models.DistanceType(chi.URLParam(r, "distanceType"))
|
||||
if distanceType == "" {
|
||||
h.logger.Warn("distance type parameter is required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Distance type parameter is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация типа дистанции
|
||||
validDistances := map[models.DistanceType]bool{
|
||||
models.Distance5K: true,
|
||||
models.Distance10K: true,
|
||||
models.DistanceHalf: true,
|
||||
models.DistanceFull: true,
|
||||
models.DistanceOther: true,
|
||||
}
|
||||
|
||||
if !validDistances[distanceType] {
|
||||
h.logger.Warn("invalid distance type", zap.String("distance_type", string(distanceType)))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid distance type")
|
||||
return
|
||||
}
|
||||
|
||||
personalBests, err := h.personalBestService.GetPersonalBestsByDistance(user.ID, distanceType)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get personal bests by distance", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal bests: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("personal bests by distance retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("distance_type", string(distanceType)),
|
||||
zap.Int("count", len(personalBests)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, personalBests)
|
||||
}
|
||||
|
||||
// GetBestByDistance возвращает лучший результат на дистанции
|
||||
func (h *PersonalBestHandler) GetBestByDistance(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get best by distance request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get best by distance failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
distanceType := models.DistanceType(chi.URLParam(r, "distanceType"))
|
||||
if distanceType == "" {
|
||||
h.logger.Warn("distance type parameter is required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Distance type parameter is required")
|
||||
return
|
||||
}
|
||||
|
||||
best, err := h.personalBestService.GetBestByDistance(user.ID, distanceType)
|
||||
if err != nil {
|
||||
if err.Error() == "record not found" {
|
||||
h.logger.Info("no personal best found for distance",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("distance_type", string(distanceType)),
|
||||
)
|
||||
utils.RespondWithJSON(w, http.StatusOK, nil)
|
||||
return
|
||||
}
|
||||
h.logger.Error("failed to get best by distance", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get best result: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("best by distance retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("distance_type", string(distanceType)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, best)
|
||||
}
|
||||
|
||||
// GetPersonalBestsSummary возвращает сводку лучших результатов
|
||||
func (h *PersonalBestHandler) GetPersonalBestsSummary(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get personal bests summary request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get personal bests summary failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
summary, err := h.personalBestService.GetPersonalBestsSummary(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get personal bests summary", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get personal bests summary: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("personal bests summary retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, summary)
|
||||
}
|
||||
|
||||
// VerifyPersonalBest подтверждает личный рекорд
|
||||
func (h *PersonalBestHandler) VerifyPersonalBest(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling verify personal best request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("verify personal best failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid personal best ID", zap.String("id", idStr))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid personal best ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.personalBestService.VerifyPersonalBest(uint(id), user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to verify personal best", zap.Error(err))
|
||||
if err.Error() == "record not found" {
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Personal best not found or access denied")
|
||||
} else {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to verify personal best: "+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("personal best verified successfully",
|
||||
zap.Uint("personal_best_id", uint(id)),
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Personal best verified successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetRecentPersonalBests возвращает последние личные рекорды
|
||||
func (h *PersonalBestHandler) GetRecentPersonalBests(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get recent personal bests request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get recent personal bests failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
limit := 10 // default limit
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
if limitStr != "" {
|
||||
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 {
|
||||
limit = l
|
||||
}
|
||||
}
|
||||
|
||||
personalBests, err := h.personalBestService.GetRecentPersonalBests(user.ID, limit)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get recent personal bests", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get recent personal bests: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("recent personal bests retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("limit", limit),
|
||||
zap.Int("count", len(personalBests)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, personalBests)
|
||||
}
|
||||
|
||||
// CalculatePace вычисляет темп
|
||||
func (h *PersonalBestHandler) CalculatePace(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling calculate pace request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
var req struct {
|
||||
Time string `json:"time"`
|
||||
DistanceType models.DistanceType `json:"distance_type"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if req.Time == "" || req.DistanceType == "" {
|
||||
h.logger.Warn("time and distance type are required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Time and distance type are required")
|
||||
return
|
||||
}
|
||||
|
||||
pace, err := h.personalBestService.CalculatePace(req.Time, req.DistanceType)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to calculate pace", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Failed to calculate pace: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("pace calculated successfully",
|
||||
zap.String("time", req.Time),
|
||||
zap.String("distance_type", string(req.DistanceType)),
|
||||
zap.String("pace", pace),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"time": req.Time,
|
||||
"distance_type": req.DistanceType,
|
||||
"pace": pace,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// handlers/review_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ReviewHandler обрабатывает HTTP-запросы, связанные с отзывами
|
||||
type ReviewHandler struct {
|
||||
reviewService service.ReviewService // Сервис для работы с отзывами
|
||||
logger logger.LoggerInterface // Логгер для записи событий
|
||||
}
|
||||
|
||||
// NewReviewHandler создает новый экземпляр ReviewHandler
|
||||
func NewReviewHandler(reviewService service.ReviewService, logger logger.LoggerInterface) *ReviewHandler {
|
||||
return &ReviewHandler{
|
||||
reviewService: reviewService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetReviews возвращает список отзывов с пагинацией и фильтрацией
|
||||
func (h *ReviewHandler) GetReviews(w http.ResponseWriter, r *http.Request) {
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
sortBy := r.URL.Query().Get("sort")
|
||||
filter := r.URL.Query().Get("filter")
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 6
|
||||
}
|
||||
|
||||
reviews, totalPages, err := h.reviewService.GetAllReviews(page, limit, sortBy, filter)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get reviews", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get reviews")
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"reviews": reviews,
|
||||
"current_page": page,
|
||||
"total_pages": totalPages,
|
||||
"total_items": len(reviews),
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetReviewsStats возвращает статистику отзывов
|
||||
func (h *ReviewHandler) GetReviewsStats(w http.ResponseWriter, r *http.Request) {
|
||||
stats, err := h.reviewService.GetReviewsStats()
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get reviews stats", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get reviews statistics")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// GetMyReviews возвращает отзывы текущего аутентифицированного пользователя
|
||||
func (h *ReviewHandler) GetMyReviews(w http.ResponseWriter, r *http.Request) {
|
||||
// Получаем ID пользователя из контекста (добавляется middleware аутентификации)
|
||||
userID, ok := r.Context().Value("middleware.UserIDKey").(uint)
|
||||
if !ok {
|
||||
h.logger.Warn("Failed to get userID from context in GetMyReviews",
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("method", r.Method),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем отзывы пользователя из сервиса
|
||||
reviews, err := h.reviewService.GetUserReviews(userID)
|
||||
if err != nil {
|
||||
h.logger.With(zap.Int("userID", int(userID))).Error("Failed to get user reviews", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get your reviews")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, reviews)
|
||||
}
|
||||
|
||||
// CreateReview создает новый отзыв от имени текущего пользователя
|
||||
func (h *ReviewHandler) CreateReview(w http.ResponseWriter, r *http.Request) {
|
||||
// Получаем ID пользователя из контекста
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
if !ok {
|
||||
h.logger.Warn("Failed to get userID from context in CreateReview",
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("method", r.Method),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
zap.Uint("userID", userID),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("Successfully extracted userID from context",
|
||||
zap.Uint("userID", userID),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("method", r.Method),
|
||||
)
|
||||
|
||||
// Декодируем тело запроса
|
||||
var req models.CreateReviewRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Error("Failed to decode review request",
|
||||
zap.Error(err),
|
||||
zap.Uint("userID", userID),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем отзыв через сервис
|
||||
review, err := h.reviewService.CreateReview(&req, userID)
|
||||
if err != nil {
|
||||
h.logger.With(zap.Int("userID", int(userID))).Error("Failed to create review", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create review")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Review created successfully",
|
||||
zap.Uint("userID", userID),
|
||||
zap.Any("review_id", review.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, review)
|
||||
}
|
||||
|
||||
// GetReviewByID возвращает отзыв по его идентификатору
|
||||
func (h *ReviewHandler) GetReviewByID(w http.ResponseWriter, r *http.Request) {
|
||||
// Получаем ID отзыва из параметров URL
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid review ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем отзыв из сервиса
|
||||
review, err := h.reviewService.GetReviewByID(uint(id))
|
||||
if err != nil {
|
||||
h.logger.With(zap.Int("id", int(id))).Error("Failed to get review", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Review not found")
|
||||
return
|
||||
}
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, review)
|
||||
}
|
||||
|
||||
// UpdateReview обновляет существующий отзыв
|
||||
func (h *ReviewHandler) UpdateReview(w http.ResponseWriter, r *http.Request) {
|
||||
// Получаем ID пользователя из контекста
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
if !ok {
|
||||
h.logger.Warn("Failed to get userID from context in UpdateReview",
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("method", r.Method),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
zap.Uint("userID", userID),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем флаг администратора из контекста
|
||||
isAdmin, _ := r.Context().Value("IsAdmin").(bool)
|
||||
|
||||
// Получаем ID отзыва из параметров URL
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid review ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Декодируем тело запроса
|
||||
var req models.UpdateReviewRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Error("Failed to decode update review request",
|
||||
zap.Error(err),
|
||||
zap.Uint("userID", userID),
|
||||
zap.Uint("review_id", uint(id)),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем отзыв через сервис
|
||||
review, err := h.reviewService.UpdateReview(uint(id), &req, userID, isAdmin)
|
||||
if err != nil {
|
||||
h.logger.With(zap.Int("id", int(id))).With(zap.Int("userID", int(userID))).Error("Failed to update review", zap.Error(err))
|
||||
if err.Error() == "unauthorized" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "You can only update your own reviews")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update review")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Review updated successfully",
|
||||
zap.Uint("userID", userID),
|
||||
zap.Uint("review_id", uint(id)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, review)
|
||||
}
|
||||
|
||||
// DeleteReview удаляет отзыв
|
||||
func (h *ReviewHandler) DeleteReview(w http.ResponseWriter, r *http.Request) {
|
||||
// Получаем ID пользователя из контекста
|
||||
userID, ok := r.Context().Value(middleware.UserIDKey).(uint)
|
||||
if !ok {
|
||||
h.logger.Warn("Failed to get userID from context in DeleteReview",
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("method", r.Method),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "User not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем флаг администратора из контекста
|
||||
isAdmin, _ := r.Context().Value("IsAdmin").(bool)
|
||||
|
||||
// Получаем ID отзыва из параметров URL
|
||||
idStr := chi.URLParam(r, "id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid review ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Удаляем отзыв через сервис
|
||||
err = h.reviewService.DeleteReview(uint(id), userID, isAdmin)
|
||||
if err != nil {
|
||||
h.logger.With(zap.Int("id", int(id))).With(zap.Int("userID", int(userID))).Error("Failed to delete review", zap.Error(err))
|
||||
if err.Error() == "unauthorized" {
|
||||
utils.RespondWithError(w, http.StatusForbidden, "You can only delete your own reviews")
|
||||
return
|
||||
}
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete review")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("Review deleted successfully",
|
||||
zap.Uint("userID", userID),
|
||||
zap.Uint("review_id", uint(id)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{"message": "Review deleted successfully"})
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
// handlers/training_plan_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type TrainingPlanHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
trainingPlanService service.TrainingPlanService
|
||||
}
|
||||
|
||||
func NewTrainingPlanHandler(trainingPlanService service.TrainingPlanService) *TrainingPlanHandler {
|
||||
return &TrainingPlanHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "training_plan"))),
|
||||
trainingPlanService: trainingPlanService,
|
||||
}
|
||||
}
|
||||
|
||||
// TrainingPlanResponse - DTO для ответа с планом тренировок
|
||||
type TrainingPlanResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Weeks int `json:"weeks"`
|
||||
WorkoutsPerWeek int `json:"workouts_per_week"`
|
||||
TargetDistance string `json:"target_distance"`
|
||||
TargetDate time.Time `json:"target_date"`
|
||||
CurrentWeek int `json:"current_week"`
|
||||
Completed bool `json:"completed"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Workouts []TrainingWorkoutResponse `json:"workouts,omitempty"`
|
||||
}
|
||||
|
||||
// TrainingWorkoutResponse - DTO для ответа с тренировкой плана
|
||||
type TrainingWorkoutResponse struct {
|
||||
ID uint `json:"id"`
|
||||
PlanID uint `json:"plan_id"`
|
||||
Week int `json:"week"`
|
||||
Day int `json:"day"`
|
||||
Type models.WorkoutType `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Distance float64 `json:"distance_km"`
|
||||
Duration int `json:"duration_min"`
|
||||
Completed bool `json:"completed"`
|
||||
CompletedAt *time.Time `json:"completed_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// CreateTrainingPlan создает новый план тренировок
|
||||
func (h *TrainingPlanHandler) CreateTrainingPlan(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling create training plan request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("create training plan failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.TrainingPlanCreateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("creating training plan",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("title", req.Title),
|
||||
zap.Int("weeks", req.Weeks),
|
||||
zap.Int("workouts_per_week", req.WorkoutsPerWeek),
|
||||
)
|
||||
|
||||
// Создаем план тренировок через сервис
|
||||
plan, err := h.trainingPlanService.CreateTrainingPlan(user.ID, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to create training plan in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create training plan: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("training plan created successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", plan.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "Training plan created successfully",
|
||||
"plan": toTrainingPlanResponse(plan),
|
||||
})
|
||||
}
|
||||
|
||||
// GetTrainingPlans возвращает все планы тренировок пользователя
|
||||
func (h *TrainingPlanHandler) GetTrainingPlans(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("handling get training plans request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get training plans failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("getting training plans for user", zap.Uint("user_id", user.ID))
|
||||
|
||||
// Получаем планы тренировок через сервис
|
||||
plans, err := h.trainingPlanService.GetTrainingPlansByUserID(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get training plans from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get training plans: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Преобразуем в response формат
|
||||
var planResponses []TrainingPlanResponse
|
||||
for _, plan := range plans {
|
||||
planResponses = append(planResponses, toTrainingPlanResponse(&plan))
|
||||
}
|
||||
|
||||
h.logger.Debug("training plans retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("plans_count", len(planResponses)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, planResponses)
|
||||
}
|
||||
|
||||
// GetTrainingPlanByID возвращает план тренировок по ID
|
||||
func (h *TrainingPlanHandler) GetTrainingPlanByID(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("handling get training plan by ID request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get training plan failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID плана из URL параметров
|
||||
planIDStr := r.URL.Query().Get("id")
|
||||
if planIDStr == "" {
|
||||
h.logger.Warn("get training plan failed - plan ID required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
planID, err := strconv.ParseUint(planIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("get training plan failed - invalid plan ID",
|
||||
zap.String("plan_id", planIDStr),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("getting training plan by ID",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
)
|
||||
|
||||
// Получаем план тренировок через сервис
|
||||
plan, err := h.trainingPlanService.GetTrainingPlanByID(user.ID, uint(planID))
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get training plan from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get training plan: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("training plan retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, toTrainingPlanResponse(plan))
|
||||
}
|
||||
|
||||
// UpdateTrainingPlan обновляет план тренировок
|
||||
func (h *TrainingPlanHandler) UpdateTrainingPlan(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling update training plan request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update training plan failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID плана из URL параметров
|
||||
planIDStr := r.URL.Query().Get("id")
|
||||
if planIDStr == "" {
|
||||
h.logger.Warn("update training plan failed - plan ID required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
planID, err := strconv.ParseUint(planIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("update training plan failed - invalid plan ID",
|
||||
zap.String("plan_id", planIDStr),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.TrainingPlanUpdateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("updating training plan",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
zap.String("title", req.Title),
|
||||
)
|
||||
|
||||
// Обновляем план тренировок через сервис
|
||||
plan, err := h.trainingPlanService.UpdateTrainingPlan(user.ID, uint(planID), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to update training plan in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update training plan: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("training plan updated successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Training plan updated successfully",
|
||||
"plan": toTrainingPlanResponse(plan),
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTrainingPlan удаляет план тренировок
|
||||
func (h *TrainingPlanHandler) DeleteTrainingPlan(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling delete training plan request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("delete training plan failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID плана из URL параметров
|
||||
planIDStr := r.URL.Query().Get("id")
|
||||
if planIDStr == "" {
|
||||
h.logger.Warn("delete training plan failed - plan ID required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
planID, err := strconv.ParseUint(planIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("delete training plan failed - invalid plan ID",
|
||||
zap.String("plan_id", planIDStr),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("deleting training plan",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
)
|
||||
|
||||
// Удаляем план тренировок через сервис
|
||||
if err := h.trainingPlanService.DeleteTrainingPlan(user.ID, uint(planID)); err != nil {
|
||||
h.logger.Error("failed to delete training plan in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete training plan: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("training plan deleted successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Training plan deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetActiveTrainingPlan возвращает активный план тренировок пользователя
|
||||
func (h *TrainingPlanHandler) GetActiveTrainingPlan(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Debug("handling get active training plan request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get active training plan failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("getting active training plan for user", zap.Uint("user_id", user.ID))
|
||||
|
||||
// Получаем активный план тренировок через сервис
|
||||
plan, err := h.trainingPlanService.GetActiveTrainingPlan(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get active training plan from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get active training plan: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Debug("active training plan retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", plan.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, toTrainingPlanResponse(plan))
|
||||
}
|
||||
|
||||
// MarkTrainingPlanAsCompleted помечает план тренировок как завершенный
|
||||
func (h *TrainingPlanHandler) MarkTrainingPlanAsCompleted(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling mark training plan as completed request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("mark training plan as completed failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID плана из URL параметров
|
||||
planIDStr := r.URL.Query().Get("id")
|
||||
if planIDStr == "" {
|
||||
h.logger.Warn("mark training plan as completed failed - plan ID required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
planID, err := strconv.ParseUint(planIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("mark training plan as completed failed - invalid plan ID",
|
||||
zap.String("plan_id", planIDStr),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("marking training plan as completed",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
)
|
||||
|
||||
// Помечаем план как завершенный через сервис
|
||||
if err := h.trainingPlanService.MarkTrainingPlanAsCompleted(user.ID, uint(planID)); err != nil {
|
||||
h.logger.Error("failed to mark training plan as completed in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to mark training plan as completed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("training plan marked as completed successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Training plan marked as completed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateCurrentWeek обновляет текущую неделю плана тренировок
|
||||
func (h *TrainingPlanHandler) UpdateCurrentWeek(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling update current week request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update current week failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Извлекаем ID плана из URL параметров
|
||||
planIDStr := r.URL.Query().Get("id")
|
||||
if planIDStr == "" {
|
||||
h.logger.Warn("update current week failed - plan ID required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Plan ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
planID, err := strconv.ParseUint(planIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("update current week failed - invalid plan ID",
|
||||
zap.String("plan_id", planIDStr),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid plan ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
CurrentWeek int `json:"current_week" validate:"required,min=1,max=52"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("updating current week for training plan",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
zap.Int("current_week", req.CurrentWeek),
|
||||
)
|
||||
|
||||
// Обновляем текущую неделю через сервис
|
||||
if err := h.trainingPlanService.UpdateCurrentWeek(user.ID, uint(planID), req.CurrentWeek); err != nil {
|
||||
h.logger.Error("failed to update current week in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update current week: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("current week updated successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("plan_id", uint(planID)),
|
||||
zap.Int("current_week", req.CurrentWeek),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Current week updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// Вспомогательные функции для преобразования моделей в DTO
|
||||
|
||||
func toTrainingPlanResponse(plan *models.TrainingPlan) TrainingPlanResponse {
|
||||
response := TrainingPlanResponse{
|
||||
ID: plan.ID,
|
||||
UserID: plan.UserID,
|
||||
Title: plan.Title,
|
||||
Description: plan.Description,
|
||||
Weeks: plan.Weeks,
|
||||
WorkoutsPerWeek: plan.WorkoutsPerWeek,
|
||||
TargetDistance: plan.TargetDistance,
|
||||
TargetDate: plan.TargetDate,
|
||||
CurrentWeek: plan.CurrentWeek,
|
||||
Completed: plan.Completed,
|
||||
CreatedAt: plan.CreatedAt,
|
||||
UpdatedAt: plan.UpdatedAt,
|
||||
}
|
||||
|
||||
// Преобразуем тренировки, если они загружены
|
||||
if plan.Workouts != nil {
|
||||
for _, workout := range plan.Workouts {
|
||||
response.Workouts = append(response.Workouts, toTrainingWorkoutResponse(&workout))
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
func toTrainingWorkoutResponse(workout *models.TrainingWorkout) TrainingWorkoutResponse {
|
||||
return TrainingWorkoutResponse{
|
||||
ID: workout.ID,
|
||||
PlanID: workout.PlanID,
|
||||
Week: workout.Week,
|
||||
Day: workout.Day,
|
||||
Type: workout.Type,
|
||||
Description: workout.Description,
|
||||
Distance: workout.Distance,
|
||||
Duration: workout.Duration,
|
||||
Completed: workout.Completed,
|
||||
CompletedAt: workout.CompletedAt,
|
||||
CreatedAt: workout.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// handlers/user.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
userService service.UserService
|
||||
}
|
||||
|
||||
func NewUserHandler(userService service.UserService) *UserHandler {
|
||||
return &UserHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user"))),
|
||||
userService: userService,
|
||||
}
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Avatar string `json:"avatar"`
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// GetUsers возвращает список всех пользователей
|
||||
func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get users request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста для проверки аутентификации
|
||||
_, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get users failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем список пользователей из сервиса
|
||||
users, err := h.userService.GetAllUsers()
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get users from service", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get users: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Преобразуем в response формат
|
||||
var userResponses []UserResponse
|
||||
for _, user := range users {
|
||||
userResponses = append(userResponses, toUserResponse(&user))
|
||||
}
|
||||
|
||||
h.logger.Info("users list retrieved successfully",
|
||||
zap.Int("users_count", len(userResponses)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, userResponses)
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
h.logger.Info("handling get profile request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get profile failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("profile retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", user.Email),
|
||||
zap.String("avatar", user.Avatar),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, toUserResponse(user))
|
||||
}
|
||||
|
||||
type UpdateProfileRequest struct {
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
h.logger.Info("handling update profile request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Логируем тело запроса для отладки
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to read request body", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Failed to read request body: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Восстанавливаем тело для дальнейшего использования
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
|
||||
h.logger.Debug("raw request body", zap.String("body", string(bodyBytes)))
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
currentUser, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update profile failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateProfileRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация обязательных полей
|
||||
if req.FirstName == "" {
|
||||
h.logger.Warn("update profile failed - first name required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "First name is required")
|
||||
return
|
||||
}
|
||||
if req.LastName == "" {
|
||||
h.logger.Warn("update profile failed - last name required")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Last name is required")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("updating user profile",
|
||||
zap.Uint("user_id", currentUser.ID),
|
||||
zap.String("first_name", req.FirstName),
|
||||
zap.String("last_name", req.LastName),
|
||||
zap.String("experience", req.Experience),
|
||||
zap.String("goals", req.Goals),
|
||||
zap.Bool("newsletter", req.Newsletter),
|
||||
)
|
||||
|
||||
// Обновляем данные пользователя
|
||||
updatedUser := &models.User{
|
||||
ID: currentUser.ID,
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Phone: req.Phone,
|
||||
Experience: req.Experience,
|
||||
Goals: req.Goals,
|
||||
Newsletter: req.Newsletter,
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Сохраняем обновленные данные
|
||||
if err := h.userService.UpdateProfile(updatedUser); err != nil {
|
||||
h.logger.Error("failed to update profile in service",
|
||||
zap.Uint("user_id", currentUser.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update profile: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("profile updated successfully",
|
||||
zap.Uint("user_id", currentUser.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Profile updated successfully",
|
||||
"user": toUserResponse(updatedUser),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
// handlers/user_achievement_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type UserAchievementHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
achievementService service.AchievementService
|
||||
}
|
||||
|
||||
func NewUserAchievementHandler(achievementService service.AchievementService) *UserAchievementHandler {
|
||||
return &UserAchievementHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user_achievement"))),
|
||||
achievementService: achievementService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAchievementsByType возвращает достижения по типу
|
||||
func (h *UserAchievementHandler) GetAchievementsByType(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get achievements by type request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get achievements by type failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем тип из URL параметров
|
||||
achievementType := chi.URLParam(r, "type")
|
||||
if achievementType == "" {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Achievement type is required")
|
||||
return
|
||||
}
|
||||
|
||||
// Валидируем тип достижения
|
||||
validType := models.AchievementType(achievementType)
|
||||
switch validType {
|
||||
case models.AchievementTypeDistance, models.AchievementTypeSpeed,
|
||||
models.AchievementTypeConsistency, models.AchievementTypeEvent,
|
||||
models.AchievementTypeSpecial:
|
||||
// valid type
|
||||
default:
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid achievement type")
|
||||
return
|
||||
}
|
||||
|
||||
achievements, err := h.achievementService.GetAchievementsByType(user.ID, validType)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get achievements by type",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("type", achievementType),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievements: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("achievements by type retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("type", achievementType),
|
||||
zap.Int("achievements_count", len(achievements)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, achievements)
|
||||
}
|
||||
|
||||
// GetAchievementByID возвращает достижение по ID
|
||||
func (h *UserAchievementHandler) GetAchievementByID(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get achievement by ID request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get achievement by ID failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID достижения из URL параметров
|
||||
achievementIDStr := chi.URLParam(r, "id")
|
||||
if achievementIDStr == "" {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Achievement ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
achievementID, err := strconv.ParseUint(achievementIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid achievement ID")
|
||||
return
|
||||
}
|
||||
|
||||
achievement, err := h.achievementService.GetAchievementByID(uint(achievementID), user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get achievement by ID",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("achievement_id", uint(achievementID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
if err == service.ErrAchievementNotFound {
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Achievement not found")
|
||||
} else {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievement: "+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("achievement retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("achievement_id", uint(achievementID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, achievement)
|
||||
}
|
||||
|
||||
// UpdateAchievement обновляет достижение
|
||||
func (h *UserAchievementHandler) UpdateAchievement(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling update achievement request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update achievement failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID достижения из URL параметров
|
||||
achievementIDStr := chi.URLParam(r, "id")
|
||||
if achievementIDStr == "" {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Achievement ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
achievementID, err := strconv.ParseUint(achievementIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid achievement ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.AchievementCreateRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Error("failed to decode achievement update request", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация запроса
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("achievement update validation failed", zap.Error(err))
|
||||
utils.RespondWithValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем достижение через сервис
|
||||
achievement, err := h.achievementService.UpdateAchievement(uint(achievementID), user.ID, req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to update achievement",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("achievement_id", uint(achievementID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
switch err {
|
||||
case service.ErrAchievementNotFound:
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Achievement not found")
|
||||
case service.ErrAchievementAlreadyExists:
|
||||
utils.RespondWithError(w, http.StatusConflict, "Achievement with this title already exists")
|
||||
default:
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update achievement: "+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("achievement updated successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("achievement_id", uint(achievementID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Achievement updated successfully",
|
||||
"achievement": achievement,
|
||||
})
|
||||
}
|
||||
|
||||
// GetPublicUserAchievements возвращает достижения пользователя для публичного просмотра
|
||||
func (h *UserAchievementHandler) GetPublicUserAchievements(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get public user achievements request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем userID из URL параметров
|
||||
userIDStr := r.URL.Query().Get("userID")
|
||||
if userIDStr == "" {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "User ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := strconv.ParseUint(userIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid user ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем только подтвержденные достижения для публичного просмотра
|
||||
achievements, err := h.achievementService.GetVerifiedAchievements(uint(userID))
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get public user achievements",
|
||||
zap.Uint("user_id", uint(userID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievements: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("public user achievements retrieved successfully",
|
||||
zap.Uint("user_id", uint(userID)),
|
||||
zap.Int("achievements_count", len(achievements)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, achievements)
|
||||
}
|
||||
|
||||
// GetPublicUserAchievementsSummary возвращает сводку по достижениям пользователя для публичного просмотра
|
||||
func (h *UserAchievementHandler) GetPublicUserAchievementsSummary(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get public user achievements summary request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем userID из URL параметров
|
||||
userIDStr := r.URL.Query().Get("userID")
|
||||
if userIDStr == "" {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "User ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := strconv.ParseUint(userIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid user ID")
|
||||
return
|
||||
}
|
||||
|
||||
summary, err := h.achievementService.GetUserAchievementsSummary(uint(userID))
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get public user achievements summary",
|
||||
zap.Uint("user_id", uint(userID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievements summary: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("public user achievements summary retrieved successfully",
|
||||
zap.Uint("user_id", uint(userID)),
|
||||
zap.Int("total_achievements", summary.TotalAchievements),
|
||||
zap.Int("completed", summary.Completed),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, summary)
|
||||
}
|
||||
|
||||
// GetPublicRecentAchievements возвращает последние достижения пользователя для публичного просмотра
|
||||
func (h *UserAchievementHandler) GetPublicRecentAchievements(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get public recent achievements request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем userID из URL параметров
|
||||
userIDStr := r.URL.Query().Get("userID")
|
||||
if userIDStr == "" {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "User ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := strconv.ParseUint(userIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid user ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем параметр limit из query string (по умолчанию 10)
|
||||
limit := 10
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
if limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
// Получаем только подтвержденные достижения
|
||||
achievements, err := h.achievementService.GetVerifiedRecentAchievements(uint(userID), limit)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get public recent achievements",
|
||||
zap.Uint("user_id", uint(userID)),
|
||||
zap.Int("limit", limit),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get recent achievements: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("public recent achievements retrieved successfully",
|
||||
zap.Uint("user_id", uint(userID)),
|
||||
zap.Int("achievements_count", len(achievements)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, achievements)
|
||||
}
|
||||
|
||||
// CreateAchievement создает новое достижение для пользователя
|
||||
func (h *UserAchievementHandler) CreateAchievement(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling create achievement request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("create achievement failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.AchievementCreateRequest
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Error("failed to decode achievement request", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация запроса
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("achievement validation failed", zap.Error(err))
|
||||
utils.RespondWithValidationError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Создаем достижение через сервис
|
||||
achievement, err := h.achievementService.CreateAchievement(user.ID, req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to create achievement",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
if err == service.ErrAchievementAlreadyExists {
|
||||
utils.RespondWithError(w, http.StatusConflict, "Achievement with this title already exists")
|
||||
} else {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create achievement: "+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("achievement created successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("achievement_id", achievement.ID),
|
||||
zap.String("title", achievement.Title),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "Achievement created successfully",
|
||||
"achievement": achievement,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserAchievements возвращает все достижения пользователя
|
||||
func (h *UserAchievementHandler) GetUserAchievements(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get user achievements request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get achievements failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
achievements, err := h.achievementService.GetUserAchievements(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get user achievements",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievements: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("user achievements retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("achievements_count", len(achievements)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, achievements)
|
||||
}
|
||||
|
||||
// GetUserAchievementsSummary возвращает сводку по достижениям пользователя
|
||||
func (h *UserAchievementHandler) GetUserAchievementsSummary(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get user achievements summary request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get achievements summary failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
summary, err := h.achievementService.GetUserAchievementsSummary(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get user achievements summary",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get achievements summary: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("user achievements summary retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("total_achievements", summary.TotalAchievements),
|
||||
zap.Int("completed", summary.Completed),
|
||||
zap.Float64("progress_percent", summary.ProgressPercent),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, summary)
|
||||
}
|
||||
|
||||
// GetRecentAchievements возвращает последние достижения пользователя
|
||||
func (h *UserAchievementHandler) GetRecentAchievements(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get recent achievements request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get recent achievements failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем параметр limit из query string (по умолчанию 10)
|
||||
limit := 10
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
if limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
achievements, err := h.achievementService.GetRecentAchievements(user.ID, limit)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get recent achievements",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("limit", limit),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get recent achievements: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("recent achievements retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("achievements_count", len(achievements)),
|
||||
zap.Int("limit", limit),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, achievements)
|
||||
}
|
||||
|
||||
// VerifyAchievement подтверждает достижение пользователя
|
||||
func (h *UserAchievementHandler) VerifyAchievement(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling verify achievement request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("verify achievement failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID достижения из URL параметров
|
||||
achievementIDStr := r.URL.Query().Get("id")
|
||||
if achievementIDStr == "" {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Achievement ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
achievementID, err := strconv.ParseUint(achievementIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid achievement ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.achievementService.VerifyAchievement(uint(achievementID), user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to verify achievement",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("achievement_id", uint(achievementID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
if err == service.ErrAchievementNotFound {
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Achievement not found")
|
||||
} else {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to verify achievement: "+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("achievement verified successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("achievement_id", uint(achievementID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Achievement verified successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAchievement удаляет достижение пользователя
|
||||
func (h *UserAchievementHandler) DeleteAchievement(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling delete achievement request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("delete achievement failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID достижения из URL параметров
|
||||
achievementIDStr := r.URL.Query().Get("id")
|
||||
if achievementIDStr == "" {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Achievement ID is required")
|
||||
return
|
||||
}
|
||||
|
||||
achievementID, err := strconv.ParseUint(achievementIDStr, 10, 32)
|
||||
if err != nil {
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid achievement ID")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.achievementService.DeleteAchievement(uint(achievementID), user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to delete achievement",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("achievement_id", uint(achievementID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
if err == service.ErrAchievementNotFound {
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Achievement not found")
|
||||
} else {
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete achievement: "+err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("achievement deleted successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("achievement_id", uint(achievementID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Achievement deleted successfully",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
// handlers/user_stats_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type UserStatsHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
userStatsService service.UserStatsService
|
||||
}
|
||||
|
||||
func NewUserStatsHandler(userStatsService service.UserStatsService) *UserStatsHandler {
|
||||
return &UserStatsHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user_stats"))),
|
||||
userStatsService: userStatsService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserStats возвращает статистику текущего пользователя
|
||||
func (h *UserStatsHandler) GetUserStats(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get user stats request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get user stats failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем статистику через сервис
|
||||
stats, err := h.userStatsService.GetUserStats(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get user stats from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user stats: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("user stats retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Float64("total_distance", stats.TotalDistance),
|
||||
zap.Int("workouts_count", stats.WorkoutsCount),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// GetUserStatsByID возвращает статистику пользователя по ID (для администраторов)
|
||||
func (h *UserStatsHandler) GetUserStatsByID(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get user stats by ID request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем текущего пользователя для проверки прав
|
||||
currentUser, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get user stats by ID failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем права администратора
|
||||
if currentUser.Role != "admin" {
|
||||
h.logger.Warn("get user stats by ID failed - insufficient permissions",
|
||||
zap.Uint("user_id", currentUser.ID),
|
||||
zap.String("role", currentUser.Role),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusForbidden, "Insufficient permissions")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID пользователя из параметров URL
|
||||
userIDStr := chi.URLParam(r, "userID")
|
||||
userID, err := strconv.ParseUint(userIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid user ID parameter",
|
||||
zap.String("user_id_param", userIDStr),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid user ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем статистику через сервис
|
||||
stats, err := h.userStatsService.GetUserStats(uint(userID))
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get user stats by ID from service",
|
||||
zap.Uint("target_user_id", uint(userID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get user stats: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("user stats by ID retrieved successfully",
|
||||
zap.Uint("admin_user_id", currentUser.ID),
|
||||
zap.Uint("target_user_id", uint(userID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// UpdatePersonalBest обновляет личный рекорд пользователя
|
||||
func (h *UserStatsHandler) UpdatePersonalBest(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling update personal best request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update personal best failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
DistanceType string `json:"distance_type"`
|
||||
Time string `json:"time"`
|
||||
}
|
||||
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Error("failed to decode update personal best request",
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация обязательных полей
|
||||
if req.DistanceType == "" || req.Time == "" {
|
||||
h.logger.Warn("update personal best failed - missing required fields")
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Distance type and time are required")
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация типа дистанции
|
||||
validDistanceTypes := map[string]bool{
|
||||
"5k": true, "10k": true, "half": true, "marathon": true,
|
||||
}
|
||||
if !validDistanceTypes[req.DistanceType] {
|
||||
h.logger.Warn("update personal best failed - invalid distance type",
|
||||
zap.String("distance_type", req.DistanceType),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid distance type. Must be: 5k, 10k, half, marathon")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("updating personal best",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("distance_type", req.DistanceType),
|
||||
zap.String("time", req.Time),
|
||||
)
|
||||
|
||||
// Обновляем личный рекорд через сервис
|
||||
if err := h.userStatsService.UpdatePersonalBest(user.ID, req.DistanceType, req.Time); err != nil {
|
||||
h.logger.Error("failed to update personal best in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update personal best: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("personal best updated successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("distance_type", req.DistanceType),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Personal best updated successfully",
|
||||
"distance_type": req.DistanceType,
|
||||
"time": req.Time,
|
||||
})
|
||||
}
|
||||
|
||||
// IncrementWorkout увеличивает счетчик тренировок и обновляет статистику
|
||||
func (h *UserStatsHandler) IncrementWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling increment workout request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("increment workout failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Distance float64 `json:"distance"`
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
if err := utils.DecodeJSONBody(w, r, &req); err != nil {
|
||||
h.logger.Error("failed to decode increment workout request",
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация данных тренировки
|
||||
if req.Distance <= 0 {
|
||||
h.logger.Warn("increment workout failed - invalid distance",
|
||||
zap.Float64("distance", req.Distance),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Distance must be greater than 0")
|
||||
return
|
||||
}
|
||||
if req.Duration <= 0 {
|
||||
h.logger.Warn("increment workout failed - invalid duration",
|
||||
zap.Int("duration", req.Duration),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Duration must be greater than 0")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("incrementing workout stats",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Float64("distance", req.Distance),
|
||||
zap.Int("duration", req.Duration),
|
||||
)
|
||||
|
||||
// Обновляем статистику через сервис
|
||||
if err := h.userStatsService.IncrementWorkout(user.ID, req.Distance, req.Duration); err != nil {
|
||||
h.logger.Error("failed to increment workout in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update workout stats: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout stats incremented successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Float64("distance", req.Distance),
|
||||
zap.Int("duration", req.Duration),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Workout stats updated successfully",
|
||||
"distance": req.Distance,
|
||||
"duration": req.Duration,
|
||||
})
|
||||
}
|
||||
|
||||
// ResetWeeklyDistance сбрасывает недельный пробег
|
||||
func (h *UserStatsHandler) ResetWeeklyDistance(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling reset weekly distance request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("reset weekly distance failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("resetting weekly distance",
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
// Сбрасываем недельный пробег через сервис
|
||||
if err := h.userStatsService.ResetWeeklyDistance(user.ID); err != nil {
|
||||
h.logger.Error("failed to reset weekly distance in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to reset weekly distance: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("weekly distance reset successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Weekly distance reset successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// ResetMonthlyDistance сбрасывает месячный пробег
|
||||
func (h *UserStatsHandler) ResetMonthlyDistance(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling reset monthly distance request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("reset monthly distance failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("resetting monthly distance",
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
// Сбрасываем месячный пробег через сервис
|
||||
if err := h.userStatsService.ResetMonthlyDistance(user.ID); err != nil {
|
||||
h.logger.Error("failed to reset monthly distance in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to reset monthly distance: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("monthly distance reset successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Monthly distance reset successfully",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
// handlers/user_workout_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
"api_bb/pkg/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type UserWorkoutHandler struct {
|
||||
logger logger.LoggerInterface
|
||||
workoutService service.WorkoutService
|
||||
}
|
||||
|
||||
func NewUserWorkoutHandler(workoutService service.WorkoutService) *UserWorkoutHandler {
|
||||
return &UserWorkoutHandler{
|
||||
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user_workout"))),
|
||||
workoutService: workoutService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateWorkout создает новую тренировку
|
||||
func (h *UserWorkoutHandler) CreateWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling create workout request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("create workout failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.WorkoutCreateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("create workout failed - validation error", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("creating new workout",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("type", string(req.Type)),
|
||||
zap.Float64("distance", req.Distance),
|
||||
zap.Int("duration", req.Duration),
|
||||
)
|
||||
|
||||
// Создаем тренировку
|
||||
workout, err := h.workoutService.CreateWorkout(user.ID, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to create workout in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to create workout: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout created successfully",
|
||||
zap.Uint("workout_id", workout.ID),
|
||||
zap.Uint("user_id", user.ID),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"message": "Workout created successfully",
|
||||
"workout": workout,
|
||||
})
|
||||
}
|
||||
|
||||
// GetWorkouts возвращает список тренировок пользователя
|
||||
func (h *UserWorkoutHandler) GetWorkouts(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get workouts request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get workouts failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
workouts, err := h.workoutService.GetUserWorkouts(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get user workouts from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get workouts: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("user workouts retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("workouts_count", len(workouts)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, workouts)
|
||||
}
|
||||
|
||||
// GetWorkoutByID возвращает тренировку по ID
|
||||
func (h *UserWorkoutHandler) GetWorkoutByID(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get workout by ID request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get workout failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID тренировки из URL параметров
|
||||
workoutIDStr := chi.URLParam(r, "id")
|
||||
workoutID, err := strconv.ParseUint(workoutIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid workout ID", zap.String("workout_id", workoutIDStr))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout ID")
|
||||
return
|
||||
}
|
||||
|
||||
workout, err := h.workoutService.GetWorkoutByID(user.ID, uint(workoutID))
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get workout from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusNotFound, "Workout not found: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, workout)
|
||||
}
|
||||
|
||||
// UpdateWorkout обновляет тренировку
|
||||
func (h *UserWorkoutHandler) UpdateWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling update workout request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("update workout failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID тренировки из URL параметров
|
||||
workoutIDStr := chi.URLParam(r, "id")
|
||||
workoutID, err := strconv.ParseUint(workoutIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid workout ID", zap.String("workout_id", workoutIDStr))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout ID")
|
||||
return
|
||||
}
|
||||
|
||||
var req models.WorkoutUpdateRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.logger.Error("failed to decode JSON payload", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid request payload: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Валидация
|
||||
if err := utils.ValidateStruct(req); err != nil {
|
||||
h.logger.Warn("update workout failed - validation error", zap.Error(err))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Validation error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("updating workout",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
zap.String("type", string(req.Type)),
|
||||
)
|
||||
|
||||
// Обновляем тренировку
|
||||
workout, err := h.workoutService.UpdateWorkout(user.ID, uint(workoutID), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to update workout in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to update workout: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout updated successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Workout updated successfully",
|
||||
"workout": workout,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteWorkout удаляет тренировку
|
||||
func (h *UserWorkoutHandler) DeleteWorkout(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling delete workout request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("delete workout failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем ID тренировки из URL параметров
|
||||
workoutIDStr := chi.URLParam(r, "id")
|
||||
workoutID, err := strconv.ParseUint(workoutIDStr, 10, 32)
|
||||
if err != nil {
|
||||
h.logger.Warn("invalid workout ID", zap.String("workout_id", workoutIDStr))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout ID")
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("deleting workout",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
)
|
||||
|
||||
// Удаляем тренировку
|
||||
if err := h.workoutService.DeleteWorkout(user.ID, uint(workoutID)); err != nil {
|
||||
h.logger.Error("failed to delete workout in service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to delete workout: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout deleted successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Uint("workout_id", uint(workoutID)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Workout deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetWorkoutStats возвращает статистику тренировок
|
||||
func (h *UserWorkoutHandler) GetWorkoutStats(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get workout stats request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get workout stats failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := h.workoutService.GetWorkoutStats(user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get workout stats from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get workout stats: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workout stats retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Int("total_workouts", stats.TotalWorkouts),
|
||||
zap.Float64("total_distance", stats.TotalDistance),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// GetWorkoutsByType возвращает тренировки по типу
|
||||
func (h *UserWorkoutHandler) GetWorkoutsByType(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("handling get workouts by type request",
|
||||
zap.String("method", r.Method),
|
||||
zap.String("path", r.URL.Path),
|
||||
zap.String("remote_addr", r.RemoteAddr),
|
||||
)
|
||||
|
||||
// Получаем пользователя из контекста
|
||||
user, ok := middleware.GetUserFromContext(r.Context())
|
||||
if !ok {
|
||||
h.logger.Warn("get workouts by type failed - authentication required")
|
||||
utils.RespondWithError(w, http.StatusUnauthorized, "Authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем тип тренировки из URL параметров
|
||||
workoutType := models.WorkoutType(chi.URLParam(r, "type"))
|
||||
|
||||
// Валидация типа тренировки
|
||||
validTypes := map[models.WorkoutType]bool{
|
||||
models.WorkoutTypeEasy: true,
|
||||
models.WorkoutTypeTempo: true,
|
||||
models.WorkoutTypeInterval: true,
|
||||
models.WorkoutTypeLong: true,
|
||||
models.WorkoutTypeRecovery: true,
|
||||
}
|
||||
|
||||
if !validTypes[workoutType] {
|
||||
h.logger.Warn("invalid workout type", zap.String("type", string(workoutType)))
|
||||
utils.RespondWithError(w, http.StatusBadRequest, "Invalid workout type")
|
||||
return
|
||||
}
|
||||
|
||||
workouts, err := h.workoutService.GetWorkoutsByType(user.ID, workoutType)
|
||||
if err != nil {
|
||||
h.logger.Error("failed to get workouts by type from service",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("type", string(workoutType)),
|
||||
zap.Error(err),
|
||||
)
|
||||
utils.RespondWithError(w, http.StatusInternalServerError, "Failed to get workouts: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("workouts by type retrieved successfully",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("type", string(workoutType)),
|
||||
zap.Int("workouts_count", len(workouts)),
|
||||
)
|
||||
|
||||
utils.RespondWithJSON(w, http.StatusOK, workouts)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// models/achievement.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AchievementType string
|
||||
|
||||
const (
|
||||
AchievementTypeDistance AchievementType = "distance"
|
||||
AchievementTypeSpeed AchievementType = "speed"
|
||||
AchievementTypeConsistency AchievementType = "consistency"
|
||||
AchievementTypeEvent AchievementType = "event"
|
||||
AchievementTypeSpecial AchievementType = "special"
|
||||
)
|
||||
|
||||
type Achievement struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||
Type AchievementType `json:"type" gorm:"type:varchar(20);not null"`
|
||||
Title string `json:"title" gorm:"size:255;not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Result string `json:"result" gorm:"size:100"` // Достигнутый результат
|
||||
Distance string `json:"distance" gorm:"size:50"` // Дистанция достижения
|
||||
Date time.Time `json:"date" gorm:"not null"`
|
||||
Verified bool `json:"verified" gorm:"default:false"`
|
||||
BadgeImage string `json:"badge_image" gorm:"size:500"` // Изображение бейджа
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Связи
|
||||
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// BeforeCreate hook
|
||||
func (a *Achievement) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.CreatedAt.IsZero() {
|
||||
a.CreatedAt = time.Now()
|
||||
}
|
||||
if a.UpdatedAt.IsZero() {
|
||||
a.UpdatedAt = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook
|
||||
func (a *Achievement) BeforeUpdate(tx *gorm.DB) error {
|
||||
a.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DTO для создания достижения
|
||||
type AchievementCreateRequest struct {
|
||||
Type AchievementType `json:"type" validate:"required,oneof=distance speed consistency event special"`
|
||||
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
Result string `json:"result" validate:"max=100"`
|
||||
Distance string `json:"distance" validate:"max=50"`
|
||||
Date time.Time `json:"date" validate:"required"`
|
||||
BadgeImage string `json:"badge_image" validate:"max=500"`
|
||||
}
|
||||
|
||||
// DTO для ответа с достижениями пользователя
|
||||
type UserAchievementsResponse struct {
|
||||
TotalAchievements int `json:"total_achievements"`
|
||||
Completed int `json:"completed"`
|
||||
ProgressPercent float64 `json:"progress_percent"`
|
||||
Achievements []Achievement `json:"achievements"`
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// models/common.go
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// Общая структура для информации об авторе
|
||||
type AuthorInfo struct {
|
||||
ID uint `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
Email string `json:"email,omitempty"` // Добавляем email
|
||||
}
|
||||
|
||||
// DTO для пагинации
|
||||
type PaginationRequest struct {
|
||||
Page int `form:"page" validate:"min=1" default:"1"`
|
||||
PerPage int `form:"per_page" validate:"min=1,max=100" default:"10"`
|
||||
}
|
||||
|
||||
type PaginationResponse struct {
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"per_page"`
|
||||
Total int `json:"total"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// DTO для фильтров
|
||||
type DateRangeFilter struct {
|
||||
StartDate *time.Time `form:"start_date"`
|
||||
EndDate *time.Time `form:"end_date"`
|
||||
}
|
||||
|
||||
type WorkoutFilter struct {
|
||||
DateRangeFilter
|
||||
Type string `form:"type"`
|
||||
UserID uint `form:"user_id"`
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// models/email.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EmailVerification struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||
Token string `json:"token" gorm:"size:100;not null;uniqueIndex"`
|
||||
Email string `json:"email" gorm:"not null"`
|
||||
Type string `json:"type" gorm:"size:20;not null"` // verification, password_reset
|
||||
ExpiresAt time.Time `json:"expires_at" gorm:"not null"`
|
||||
Used bool `json:"used" gorm:"default:false"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
|
||||
// Связи
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
||||
type PasswordResetRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
type PasswordResetConfirm struct {
|
||||
Token string `json:"token" validate:"required"`
|
||||
Password string `json:"password" validate:"required,min=6"`
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// models/event.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventTypeRace EventType = "race"
|
||||
EventTypeTraining EventType = "training"
|
||||
EventTypeSocial EventType = "social"
|
||||
EventTypeWorkshop EventType = "workshop"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Title string `gorm:"size:255;not null" json:"title" validate:"required,min=5,max=255"`
|
||||
Description string `gorm:"type:text;not null" json:"description" validate:"required,min=10"`
|
||||
Date time.Time `gorm:"not null" json:"date" validate:"required"`
|
||||
Location string `gorm:"size:255;not null" json:"location" validate:"required,max=255"`
|
||||
Type EventType `gorm:"size:50;not null" json:"type" validate:"required,oneof=race training social workshop"`
|
||||
Distance string `gorm:"size:50" json:"distance" validate:"max=50"`
|
||||
ParticipantsCount int `gorm:"default:0" json:"participants_count"`
|
||||
MaxParticipants int `gorm:"default:0" json:"max_participants" validate:"min=0"`
|
||||
RegistrationOpen bool `gorm:"default:true" json:"registration_open"`
|
||||
Image string `gorm:"size:500" json:"image" validate:"max=500"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
// Связи
|
||||
Registrations []EventRegistration `gorm:"foreignKey:EventID" json:"registrations,omitempty"`
|
||||
}
|
||||
|
||||
type EventRegistration struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null" json:"user_id"`
|
||||
EventID uint `gorm:"not null" json:"event_id"`
|
||||
Status string `gorm:"size:50;default:pending" json:"status" validate:"oneof=pending confirmed cancelled completed"`
|
||||
Notes string `gorm:"type:text" json:"notes" validate:"max=500"`
|
||||
ResultTime string `gorm:"size:20" json:"result_time" validate:"max=20"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
|
||||
|
||||
// Связи
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Event *Event `gorm:"foreignKey:EventID" json:"event,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// models/gallery.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type GalleryCategory string
|
||||
|
||||
const (
|
||||
GalleryCategoryTraining GalleryCategory = "training"
|
||||
GalleryCategoryEvents GalleryCategory = "events"
|
||||
GalleryCategoryCommunity GalleryCategory = "community"
|
||||
GalleryCategoryAchievements GalleryCategory = "achievements"
|
||||
)
|
||||
|
||||
type Gallery struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Title string `json:"title" gorm:"size:255;not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
ImagePath string `json:"image_path" gorm:"size:500;not null"` // Путь к изображению
|
||||
Category GalleryCategory `json:"category" gorm:"type:varchar(20);not null"`
|
||||
AuthorID uint `json:"author_id" gorm:"not null;index"`
|
||||
EventDate *time.Time `json:"event_date"` // Дата события на фото
|
||||
Views int `json:"views" gorm:"default:0"`
|
||||
Likes int `json:"likes" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Связи
|
||||
Author User `json:"author,omitempty" gorm:"foreignKey:AuthorID"`
|
||||
}
|
||||
|
||||
// BeforeCreate hook
|
||||
func (g *Gallery) BeforeCreate(tx *gorm.DB) error {
|
||||
if g.CreatedAt.IsZero() {
|
||||
g.CreatedAt = time.Now()
|
||||
}
|
||||
if g.UpdatedAt.IsZero() {
|
||||
g.UpdatedAt = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook
|
||||
func (g *Gallery) BeforeUpdate(tx *gorm.DB) error {
|
||||
g.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DTO для создания записи в галерее
|
||||
type GalleryCreateRequest struct {
|
||||
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
ImagePath string `json:"image_path" validate:"required,max=500"`
|
||||
Category GalleryCategory `json:"category" validate:"required,oneof=training events community achievements"`
|
||||
EventDate *time.Time `json:"event_date"`
|
||||
}
|
||||
|
||||
// DTO для ответа с галереей
|
||||
type GalleryResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
ImagePath string `json:"image_path"`
|
||||
Category GalleryCategory `json:"category"`
|
||||
EventDate *time.Time `json:"event_date"`
|
||||
Views int `json:"views"`
|
||||
Likes int `json:"likes"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Author AuthorInfo `json:"author"`
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type NewsCategory string
|
||||
|
||||
const (
|
||||
NewsCategoryEvents NewsCategory = "events"
|
||||
NewsCategoryTraining NewsCategory = "training"
|
||||
NewsCategoryAchievements NewsCategory = "achievements"
|
||||
NewsCategoryCommunity NewsCategory = "community"
|
||||
)
|
||||
|
||||
type News struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
|
||||
|
||||
Title string `json:"title" gorm:"size:255;not null"`
|
||||
Excerpt string `json:"excerpt" gorm:"size:500;not null"`
|
||||
Content string `json:"content" gorm:"type:text;not null"`
|
||||
Image string `json:"image" gorm:"size:255"`
|
||||
Category NewsCategory `json:"category" gorm:"type:varchar(20);not null"`
|
||||
Views int `json:"views" gorm:"default:0"`
|
||||
|
||||
// Связи
|
||||
AuthorID uint `json:"author_id" gorm:"not null"`
|
||||
Author User `json:"author" gorm:"foreignKey:AuthorID"`
|
||||
|
||||
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:NewsID"`
|
||||
}
|
||||
|
||||
type Comment struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
Content string `json:"content" gorm:"type:text;not null"`
|
||||
|
||||
// Связи
|
||||
NewsID uint `json:"news_id" gorm:"not null"`
|
||||
AuthorID uint `json:"author_id" gorm:"not null"`
|
||||
Author User `json:"author" gorm:"foreignKey:AuthorID"`
|
||||
}
|
||||
|
||||
// DTO для создания новости
|
||||
type CreateNewsRequest struct {
|
||||
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||
Excerpt string `json:"excerpt" validate:"required,min=10,max=500"`
|
||||
Content string `json:"content" validate:"required,min=50"`
|
||||
Image string `json:"image"`
|
||||
Category NewsCategory `json:"category" validate:"required,oneof=events training achievements community"`
|
||||
}
|
||||
|
||||
// DTO для обновления новости
|
||||
type UpdateNewsRequest struct {
|
||||
Title string `json:"title" validate:"omitempty,min=5,max=255"`
|
||||
Excerpt string `json:"excerpt" validate:"omitempty,min=10,max=500"`
|
||||
Content string `json:"content" validate:"omitempty,min=50"`
|
||||
Image string `json:"image"`
|
||||
Category NewsCategory `json:"category" validate:"omitempty,oneof=events training achievements community"`
|
||||
}
|
||||
|
||||
// DTO для ответа с новостью
|
||||
type NewsResponse struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Title string `json:"title"`
|
||||
Excerpt string `json:"excerpt"`
|
||||
Content string `json:"content"`
|
||||
Image string `json:"image"`
|
||||
Category NewsCategory `json:"category"`
|
||||
Views int `json:"views"`
|
||||
Author AuthorInfo `json:"author"`
|
||||
Comments int `json:"comments_count"`
|
||||
}
|
||||
|
||||
// DTO для комментария
|
||||
type CreateCommentRequest struct {
|
||||
Content string `json:"content" validate:"required,min=1,max=1000"`
|
||||
}
|
||||
|
||||
type CommentResponse struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Content string `json:"content"`
|
||||
Author AuthorInfo `json:"author"`
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// models/personal_best.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DistanceType string
|
||||
|
||||
const (
|
||||
Distance5K DistanceType = "5k"
|
||||
Distance10K DistanceType = "10k"
|
||||
DistanceHalf DistanceType = "half_marathon"
|
||||
DistanceFull DistanceType = "marathon"
|
||||
DistanceOther DistanceType = "other"
|
||||
)
|
||||
|
||||
type PersonalBest struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||
DistanceType DistanceType `json:"distance_type" gorm:"type:varchar(20);not null"`
|
||||
Time string `json:"time" gorm:"size:20;not null"` // Время в формате "HH:MM:SS"
|
||||
Pace string `json:"pace" gorm:"size:20"` // Темп
|
||||
Date time.Time `json:"date" gorm:"not null"`
|
||||
Verified bool `json:"verified" gorm:"default:false"` // Подтвержден ли результат
|
||||
EventName string `json:"event_name" gorm:"size:255"` // Название забега
|
||||
Location string `json:"location" gorm:"size:255"` // Место проведения
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Связи
|
||||
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// BeforeCreate hook
|
||||
func (pb *PersonalBest) BeforeCreate(tx *gorm.DB) error {
|
||||
if pb.CreatedAt.IsZero() {
|
||||
pb.CreatedAt = time.Now()
|
||||
}
|
||||
if pb.UpdatedAt.IsZero() {
|
||||
pb.UpdatedAt = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook
|
||||
func (pb *PersonalBest) BeforeUpdate(tx *gorm.DB) error {
|
||||
pb.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DTO для создания личного рекорда
|
||||
type PersonalBestCreateRequest struct {
|
||||
DistanceType DistanceType `json:"distance_type" validate:"required,oneof=5k 10k half_marathon marathon other"`
|
||||
Time string `json:"time" validate:"required,max=20"`
|
||||
Pace string `json:"pace" validate:"max=20"`
|
||||
Date time.Time `json:"date" validate:"required"`
|
||||
EventName string `json:"event_name" validate:"max=255"`
|
||||
Location string `json:"location" validate:"max=255"`
|
||||
}
|
||||
|
||||
// DTO для обновления личного рекорда
|
||||
type PersonalBestUpdateRequest struct {
|
||||
DistanceType DistanceType `json:"distance_type" validate:"omitempty,oneof=5k 10k half_marathon marathon other"`
|
||||
Time string `json:"time" validate:"omitempty,max=20"`
|
||||
Pace string `json:"pace" validate:"omitempty,max=20"`
|
||||
Date time.Time `json:"date"`
|
||||
EventName string `json:"event_name" validate:"omitempty,max=255"`
|
||||
Location string `json:"location" validate:"omitempty,max=255"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
// PersonalBestsSummary представляет сводку лучших результатов по дистанциям
|
||||
type PersonalBestsSummary struct {
|
||||
Best5K string `json:"best_5k,omitempty"`
|
||||
Best5KPace string `json:"best_5k_pace,omitempty"`
|
||||
Best10K string `json:"best_10k,omitempty"`
|
||||
Best10KPace string `json:"best_10k_pace,omitempty"`
|
||||
BestHalf string `json:"best_half_marathon,omitempty"`
|
||||
BestHalfPace string `json:"best_half_marathon_pace,omitempty"`
|
||||
BestMarathon string `json:"best_marathon,omitempty"`
|
||||
BestMarathonPace string `json:"best_marathon_pace,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// models/review.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Review struct {
|
||||
ID uint `json:"id" gorm:"primarykey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"`
|
||||
|
||||
Rating int `json:"rating" gorm:"not null;check:rating >= 1 AND rating <= 5"`
|
||||
Text string `json:"text" gorm:"type:text;not null"`
|
||||
Achievement string `json:"achievement" gorm:"size:255"`
|
||||
Distance string `json:"distance" gorm:"size:50"`
|
||||
Improvement string `json:"improvement" gorm:"size:100"`
|
||||
Trainings int `json:"trainings" gorm:"default:0"`
|
||||
Verified bool `json:"verified" gorm:"default:false"`
|
||||
|
||||
// Связи
|
||||
AuthorID uint `json:"author_id" gorm:"not null"`
|
||||
Author User `json:"author" gorm:"foreignKey:AuthorID"`
|
||||
}
|
||||
|
||||
// DTO для создания отзыва
|
||||
type CreateReviewRequest struct {
|
||||
Rating int `json:"rating" validate:"required,min=1,max=5"`
|
||||
Text string `json:"text" validate:"required,min=10,max=500"`
|
||||
Achievement string `json:"achievement" validate:"max=255"`
|
||||
Distance string `json:"distance" validate:"max=50"`
|
||||
Improvement string `json:"improvement" validate:"max=100"`
|
||||
Trainings int `json:"trainings" validate:"min=0"`
|
||||
}
|
||||
|
||||
// DTO для обновления отзыва
|
||||
type UpdateReviewRequest struct {
|
||||
Rating int `json:"rating" validate:"omitempty,min=1,max=5"`
|
||||
Text string `json:"text" validate:"omitempty,min=10,max=500"`
|
||||
Achievement string `json:"achievement" validate:"omitempty,max=255"`
|
||||
Distance string `json:"distance" validate:"omitempty,max=50"`
|
||||
Improvement string `json:"improvement" validate:"omitempty,max=100"`
|
||||
Trainings int `json:"trainings" validate:"omitempty,min=0"`
|
||||
}
|
||||
|
||||
// DTO для ответа с отзывом
|
||||
type ReviewResponse struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Rating int `json:"rating"`
|
||||
Text string `json:"text"`
|
||||
Achievement string `json:"achievement,omitempty"`
|
||||
Distance string `json:"distance,omitempty"`
|
||||
Improvement string `json:"improvement,omitempty"`
|
||||
Trainings int `json:"trainings"`
|
||||
Verified bool `json:"verified"`
|
||||
Author AuthorInfo `json:"author"`
|
||||
}
|
||||
|
||||
// DTO для статистики отзывов
|
||||
type ReviewsStatsResponse struct {
|
||||
TotalReviews int `json:"total_reviews"`
|
||||
AverageRating float64 `json:"average_rating"`
|
||||
SuccessStories int `json:"success_stories"`
|
||||
RatingDistribution map[int]int `json:"rating_distribution"`
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// models/training_plan.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TrainingPlan struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||
Title string `json:"title" gorm:"size:255;not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Weeks int `json:"weeks" gorm:"not null;default:12"` // Длительность плана в неделях
|
||||
WorkoutsPerWeek int `json:"workouts_per_week" gorm:"not null;default:3"` // Тренировок в неделю
|
||||
TargetDistance string `json:"target_distance" gorm:"size:50"` // Целевая дистанция
|
||||
TargetDate time.Time `json:"target_date"` // Дата цели
|
||||
CurrentWeek int `json:"current_week" gorm:"default:1"` // Текущая неделя
|
||||
Completed bool `json:"completed" gorm:"default:false"` // Завершен ли план
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Связи
|
||||
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Workouts []TrainingWorkout `json:"workouts,omitempty" gorm:"foreignKey:PlanID"`
|
||||
}
|
||||
|
||||
type TrainingWorkout struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
PlanID uint `json:"plan_id" gorm:"not null;index"`
|
||||
Week int `json:"week" gorm:"not null"` // Неделя плана
|
||||
Day int `json:"day" gorm:"not null"` // День недели (1-7)
|
||||
Type WorkoutType `json:"type" gorm:"type:varchar(20);not null"`
|
||||
Description string `json:"description" gorm:"type:text"`
|
||||
Distance float64 `json:"distance_km" gorm:"type:decimal(5,2)"`
|
||||
Duration int `json:"duration_min"`
|
||||
Completed bool `json:"completed" gorm:"default:false"`
|
||||
CompletedAt *time.Time `json:"completed_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// BeforeCreate hooks
|
||||
func (tp *TrainingPlan) BeforeCreate(tx *gorm.DB) error {
|
||||
if tp.CreatedAt.IsZero() {
|
||||
tp.CreatedAt = time.Now()
|
||||
}
|
||||
if tp.UpdatedAt.IsZero() {
|
||||
tp.UpdatedAt = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tw *TrainingWorkout) BeforeCreate(tx *gorm.DB) error {
|
||||
if tw.CreatedAt.IsZero() {
|
||||
tw.CreatedAt = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook
|
||||
func (tp *TrainingPlan) BeforeUpdate(tx *gorm.DB) error {
|
||||
tp.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DTO для создания плана тренировок
|
||||
type TrainingPlanCreateRequest struct {
|
||||
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
Weeks int `json:"weeks" validate:"required,min=1,max=52"`
|
||||
WorkoutsPerWeek int `json:"workouts_per_week" validate:"required,min=1,max=7"`
|
||||
TargetDistance string `json:"target_distance" validate:"max=50"`
|
||||
TargetDate time.Time `json:"target_date"`
|
||||
}
|
||||
|
||||
// DTO для обновления плана тренировок
|
||||
type TrainingPlanUpdateRequest struct {
|
||||
Title string `json:"title" validate:"min=5,max=255"`
|
||||
Description string `json:"description" validate:"max=1000"`
|
||||
Weeks int `json:"weeks" validate:"min=1,max=52"`
|
||||
WorkoutsPerWeek int `json:"workouts_per_week" validate:"min=1,max=7"`
|
||||
TargetDistance string `json:"target_distance" validate:"max=50"`
|
||||
TargetDate time.Time `json:"target_date"`
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// models/user.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// models/user.go - добавить поле Avatar
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||
Password string `json:"-" gorm:"not null"`
|
||||
FirstName string `json:"first_name" gorm:"not null"`
|
||||
LastName string `json:"last_name" gorm:"not null"`
|
||||
Avatar string `json:"avatar"` // Путь к файлу аватара
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
Role string `json:"role" gorm:"default:user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
EmailVerified bool `json:"email_verified" gorm:"default:false"`
|
||||
VerifiedAt time.Time `json:"verified_at"`
|
||||
|
||||
// Связи
|
||||
Workouts []Workout `json:"workouts,omitempty" gorm:"foreignKey:UserID"`
|
||||
PersonalBests []PersonalBest `json:"personal_bests,omitempty" gorm:"foreignKey:UserID"`
|
||||
Achievements []Achievement `json:"achievements,omitempty" gorm:"foreignKey:UserID"`
|
||||
TrainingPlans []TrainingPlan `json:"training_plans,omitempty" gorm:"foreignKey:UserID"`
|
||||
News []News `json:"news,omitempty" gorm:"foreignKey:AuthorID"`
|
||||
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:AuthorID"`
|
||||
Reviews []Review `json:"reviews,omitempty" gorm:"foreignKey:AuthorID"`
|
||||
Gallery []Gallery `json:"gallery,omitempty" gorm:"foreignKey:AuthorID"`
|
||||
EventRegistrations []EventRegistration `json:"event_registrations,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
type UserUpdate struct {
|
||||
ID uint `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// HashPassword хеширует пароль перед сохранением
|
||||
func (u *User) HashPassword() error {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hashedPassword)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPassword проверяет пароль
|
||||
func (u *User) CheckPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// BeforeCreate hook для GORM
|
||||
func (u *User) BeforeCreate(tx *gorm.DB) error {
|
||||
if u.CreatedAt.IsZero() {
|
||||
u.CreatedAt = time.Now()
|
||||
}
|
||||
if u.UpdatedAt.IsZero() {
|
||||
u.UpdatedAt = time.Now()
|
||||
}
|
||||
return u.HashPassword()
|
||||
}
|
||||
|
||||
// BeforeUpdate hook для GORM
|
||||
func (u *User) BeforeUpdate(tx *gorm.DB) error {
|
||||
u.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DTO для обновления профиля
|
||||
type UserUpdateRequest struct {
|
||||
FirstName string `json:"first_name" validate:"required,min=2,max=100"`
|
||||
LastName string `json:"last_name" validate:"required,min=2,max=100"`
|
||||
Phone string `json:"phone" validate:"max=20"`
|
||||
Experience string `json:"experience" validate:"max=50"`
|
||||
Goals string `json:"goals" validate:"max=100"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
}
|
||||
|
||||
// DTO для ответа с пользователем (без sensitive данных)
|
||||
type UserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
Role string `json:"role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// DTO для ответа с пользователем и статистикой
|
||||
type UserWithStatsResponse struct {
|
||||
UserResponse
|
||||
Stats *UserStatsResponse `json:"stats,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// models/user_stats.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserStats struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UserID uint `json:"user_id" gorm:"uniqueIndex;not null"`
|
||||
TotalDistance float64 `json:"total_distance" gorm:"type:decimal(10,2);default:0"` // Общий пробег в км
|
||||
TotalTime int `json:"total_time" gorm:"default:0"` // Общее время в минутах
|
||||
AvgPace string `json:"avg_pace" gorm:"size:20"` // Средний темп
|
||||
WorkoutsCount int `json:"workouts_count" gorm:"default:0"` // Количество тренировок
|
||||
CurrentStreak int `json:"current_streak" gorm:"default:0"` // Текущая серия дней подряд
|
||||
LongestStreak int `json:"longest_streak" gorm:"default:0"` // Самая длинная серия
|
||||
WeeklyDistance float64 `json:"weekly_distance" gorm:"type:decimal(8,2);default:0"` // Пробег за неделю
|
||||
MonthlyDistance float64 `json:"monthly_distance" gorm:"type:decimal(8,2);default:0"` // Пробег за месяц
|
||||
Best5K string `json:"best_5k" gorm:"size:20"` // Лучший результат на 5к
|
||||
Best10K string `json:"best_10k" gorm:"size:20"` // Лучший результат на 10к
|
||||
BestHalf string `json:"best_half" gorm:"size:20"` // Лучший результат на полумарафон
|
||||
BestMarathon string `json:"best_marathon" gorm:"size:20"` // Лучший результат на марафон
|
||||
LastWorkout time.Time `json:"last_workout"` // Последняя тренировка
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Связи
|
||||
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// BeforeCreate hook
|
||||
func (us *UserStats) BeforeCreate(tx *gorm.DB) error {
|
||||
if us.CreatedAt.IsZero() {
|
||||
us.CreatedAt = time.Now()
|
||||
}
|
||||
if us.UpdatedAt.IsZero() {
|
||||
us.UpdatedAt = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook
|
||||
func (us *UserStats) BeforeUpdate(tx *gorm.DB) error {
|
||||
us.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DTO для статистики пользователя
|
||||
type UserStatsResponse struct {
|
||||
TotalDistance float64 `json:"total_distance"`
|
||||
TotalTime int `json:"total_time"`
|
||||
AvgPace string `json:"avg_pace"`
|
||||
WorkoutsCount int `json:"workouts_count"`
|
||||
CurrentStreak int `json:"current_streak"`
|
||||
LongestStreak int `json:"longest_streak"`
|
||||
WeeklyDistance float64 `json:"weekly_distance"`
|
||||
MonthlyDistance float64 `json:"monthly_distance"`
|
||||
PersonalBests PersonalBestsSummary `json:"personal_bests"`
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// models/workout.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type WorkoutType string
|
||||
|
||||
const (
|
||||
WorkoutTypeEasy WorkoutType = "easy"
|
||||
WorkoutTypeTempo WorkoutType = "tempo"
|
||||
WorkoutTypeInterval WorkoutType = "interval"
|
||||
WorkoutTypeLong WorkoutType = "long"
|
||||
WorkoutTypeRecovery WorkoutType = "recovery"
|
||||
)
|
||||
|
||||
type Workout struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||
Type WorkoutType `json:"type" gorm:"type:varchar(20);not null"`
|
||||
Distance float64 `json:"distance_km" gorm:"type:decimal(5,2);not null"` // Дистанция в км
|
||||
Duration int `json:"duration_min" gorm:"not null"` // Продолжительность в минутах
|
||||
Pace string `json:"pace" gorm:"size:20"` // Темп (например, "5:30")
|
||||
Calories int `json:"calories" gorm:"default:0"` // Сожженные калории
|
||||
Notes string `json:"notes" gorm:"type:text"` // Заметки к тренировке
|
||||
Date time.Time `json:"date" gorm:"not null;index"` // Дата тренировки
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Связи
|
||||
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
}
|
||||
|
||||
// BeforeCreate hook
|
||||
func (w *Workout) BeforeCreate(tx *gorm.DB) error {
|
||||
if w.CreatedAt.IsZero() {
|
||||
w.CreatedAt = time.Now()
|
||||
}
|
||||
if w.UpdatedAt.IsZero() {
|
||||
w.UpdatedAt = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BeforeUpdate hook
|
||||
func (w *Workout) BeforeUpdate(tx *gorm.DB) error {
|
||||
w.UpdatedAt = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DTO для создания тренировки
|
||||
type WorkoutCreateRequest struct {
|
||||
Type WorkoutType `json:"type" validate:"required,oneof=easy tempo interval long recovery"`
|
||||
Distance float64 `json:"distance_km" validate:"required,min=0.1,max=1000"`
|
||||
Duration int `json:"duration_min" validate:"required,min=1,max=1440"`
|
||||
Pace string `json:"pace" validate:"maxlen=20"`
|
||||
Calories int `json:"calories" validate:"minint=0,maxint=5000"`
|
||||
Notes string `json:"notes" validate:"maxlen=1000"`
|
||||
Date time.Time `json:"date" validate:"required"`
|
||||
}
|
||||
|
||||
// DTO для обновления тренировки
|
||||
type WorkoutUpdateRequest struct {
|
||||
Type WorkoutType `json:"type" validate:"oneof=easy tempo interval long recovery"`
|
||||
Distance float64 `json:"distance_km" validate:"min=0.1,max=1000"`
|
||||
Duration int `json:"duration_min" validate:"min=1,max=1440"`
|
||||
Pace string `json:"pace" validate:"maxlen=20"`
|
||||
Calories int `json:"calories" validate:"minint=0,maxint=5000"`
|
||||
Notes string `json:"notes" validate:"maxlen=1000"`
|
||||
Date time.Time `json:"date"`
|
||||
}
|
||||
|
||||
// DTO для статистики тренировок
|
||||
type WorkoutStatsResponse struct {
|
||||
TotalWorkouts int `json:"total_workouts"`
|
||||
TotalDistance float64 `json:"total_distance_km"`
|
||||
TotalTime int `json:"total_time_min"`
|
||||
AveragePace string `json:"average_pace"`
|
||||
MonthlyStats []MonthlyStat `json:"monthly_stats"`
|
||||
}
|
||||
|
||||
type MonthlyStat struct {
|
||||
Month string `json:"month"`
|
||||
Distance float64 `json:"distance_km"`
|
||||
Workouts int `json:"workouts"`
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
// repositories/achievement_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"api_bb/internal/models"
|
||||
)
|
||||
|
||||
type AchievementRepository interface {
|
||||
Create(achievement *models.Achievement) error
|
||||
GetByID(id uint) (*models.Achievement, error)
|
||||
GetByUserID(userID uint) ([]models.Achievement, error)
|
||||
GetByUserAndType(userID uint, achievementType models.AchievementType) ([]models.Achievement, error)
|
||||
GetVerifiedByUserID(userID uint) ([]models.Achievement, error)
|
||||
GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.Achievement, error)
|
||||
Update(achievement *models.Achievement) error
|
||||
Delete(id uint) error
|
||||
VerifyAchievement(id uint) error
|
||||
GetUserAchievementsSummary(userID uint) (*models.UserAchievementsResponse, error)
|
||||
GetRecentAchievements(userID uint, limit int) ([]models.Achievement, error)
|
||||
CountByType(userID uint) (map[models.AchievementType]int64, error)
|
||||
ExistsByTitleAndUser(userID uint, title string) (bool, error)
|
||||
}
|
||||
|
||||
type achievementRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewAchievementRepository(db *gorm.DB) AchievementRepository {
|
||||
return &achievementRepository{db: db}
|
||||
}
|
||||
|
||||
// Create создает новое достижение
|
||||
func (r *achievementRepository) Create(achievement *models.Achievement) error {
|
||||
return r.db.Create(achievement).Error
|
||||
}
|
||||
|
||||
// GetByID возвращает достижение по ID
|
||||
func (r *achievementRepository) GetByID(id uint) (*models.Achievement, error) {
|
||||
var achievement models.Achievement
|
||||
err := r.db.Preload("User").First(&achievement, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &achievement, nil
|
||||
}
|
||||
|
||||
// GetByUserID возвращает все достижения пользователя
|
||||
func (r *achievementRepository) GetByUserID(userID uint) ([]models.Achievement, error) {
|
||||
var achievements []models.Achievement
|
||||
err := r.db.Where("user_id = ?", userID).
|
||||
Order("date DESC, created_at DESC").
|
||||
Find(&achievements).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return achievements, nil
|
||||
}
|
||||
|
||||
// GetByUserAndType возвращает достижения пользователя по типу
|
||||
func (r *achievementRepository) GetByUserAndType(userID uint, achievementType models.AchievementType) ([]models.Achievement, error) {
|
||||
var achievements []models.Achievement
|
||||
err := r.db.Where("user_id = ? AND type = ?", userID, achievementType).
|
||||
Order("date DESC").
|
||||
Find(&achievements).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return achievements, nil
|
||||
}
|
||||
|
||||
// GetVerifiedByUserID возвращает подтвержденные достижения пользователя
|
||||
func (r *achievementRepository) GetVerifiedByUserID(userID uint) ([]models.Achievement, error) {
|
||||
var achievements []models.Achievement
|
||||
err := r.db.Where("user_id = ? AND verified = ?", userID, true).
|
||||
Order("date DESC").
|
||||
Find(&achievements).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return achievements, nil
|
||||
}
|
||||
|
||||
// GetByDateRange возвращает достижения за период времени
|
||||
func (r *achievementRepository) GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.Achievement, error) {
|
||||
var achievements []models.Achievement
|
||||
err := r.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate).
|
||||
Order("date DESC").
|
||||
Find(&achievements).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return achievements, nil
|
||||
}
|
||||
|
||||
// Update обновляет достижение
|
||||
func (r *achievementRepository) Update(achievement *models.Achievement) error {
|
||||
return r.db.Save(achievement).Error
|
||||
}
|
||||
|
||||
// Delete удаляет достижение
|
||||
func (r *achievementRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.Achievement{}, id).Error
|
||||
}
|
||||
|
||||
// VerifyAchievement подтверждает достижение
|
||||
func (r *achievementRepository) VerifyAchievement(id uint) error {
|
||||
return r.db.Model(&models.Achievement{}).
|
||||
Where("id = ?", id).
|
||||
Update("verified", true).
|
||||
Error
|
||||
}
|
||||
|
||||
// GetUserAchievementsSummary возвращает сводку по достижениям пользователя
|
||||
func (r *achievementRepository) GetUserAchievementsSummary(userID uint) (*models.UserAchievementsResponse, error) {
|
||||
var totalCount int64
|
||||
var verifiedCount int64
|
||||
|
||||
// Считаем общее количество достижений
|
||||
err := r.db.Model(&models.Achievement{}).
|
||||
Where("user_id = ?", userID).
|
||||
Count(&totalCount).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Считаем количество подтвержденных достижений
|
||||
err = r.db.Model(&models.Achievement{}).
|
||||
Where("user_id = ? AND verified = ?", userID, true).
|
||||
Count(&verifiedCount).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Получаем все достижения пользователя
|
||||
achievements, err := r.GetByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Вычисляем процент прогресса
|
||||
progressPercent := 0.0
|
||||
if totalCount > 0 {
|
||||
progressPercent = (float64(verifiedCount) / float64(totalCount)) * 100
|
||||
}
|
||||
|
||||
return &models.UserAchievementsResponse{
|
||||
TotalAchievements: int(totalCount),
|
||||
Completed: int(verifiedCount),
|
||||
ProgressPercent: progressPercent,
|
||||
Achievements: achievements,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetRecentAchievements возвращает последние достижения пользователя
|
||||
func (r *achievementRepository) GetRecentAchievements(userID uint, limit int) ([]models.Achievement, error) {
|
||||
var achievements []models.Achievement
|
||||
err := r.db.Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Find(&achievements).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return achievements, nil
|
||||
}
|
||||
|
||||
// CountByType возвращает количество достижений по типам
|
||||
func (r *achievementRepository) CountByType(userID uint) (map[models.AchievementType]int64, error) {
|
||||
type CountResult struct {
|
||||
Type models.AchievementType
|
||||
Count int64
|
||||
}
|
||||
|
||||
var results []CountResult
|
||||
err := r.db.Model(&models.Achievement{}).
|
||||
Select("type, COUNT(*) as count").
|
||||
Where("user_id = ?", userID).
|
||||
Group("type").
|
||||
Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
counts := make(map[models.AchievementType]int64)
|
||||
for _, result := range results {
|
||||
counts[result.Type] = result.Count
|
||||
}
|
||||
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// ExistsByTitleAndUser проверяет, существует ли достижение с таким названием у пользователя
|
||||
func (r *achievementRepository) ExistsByTitleAndUser(userID uint, title string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.Achievement{}).
|
||||
Where("user_id = ? AND title = ?", userID, title).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetUnverifiedAchievements возвращает неподтвержденные достижения
|
||||
func (r *achievementRepository) GetUnverifiedAchievements(userID uint) ([]models.Achievement, error) {
|
||||
var achievements []models.Achievement
|
||||
err := r.db.Where("user_id = ? AND verified = ?", userID, false).
|
||||
Order("created_at DESC").
|
||||
Find(&achievements).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return achievements, nil
|
||||
}
|
||||
|
||||
// GetAchievementsWithPagination возвращает достижения с пагинацией
|
||||
func (r *achievementRepository) GetAchievementsWithPagination(userID uint, page, pageSize int) ([]models.Achievement, int64, error) {
|
||||
var achievements []models.Achievement
|
||||
var totalCount int64
|
||||
|
||||
// Считаем общее количество
|
||||
err := r.db.Model(&models.Achievement{}).
|
||||
Where("user_id = ?", userID).
|
||||
Count(&totalCount).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Получаем данные с пагинацией
|
||||
offset := (page - 1) * pageSize
|
||||
err = r.db.Where("user_id = ?", userID).
|
||||
Order("date DESC, created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&achievements).Error
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return achievements, totalCount, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type CommentRepository interface {
|
||||
Create(comment *models.Comment) error
|
||||
GetByNewsID(newsID uint) ([]models.Comment, error)
|
||||
Delete(id uint) error
|
||||
GetByID(id uint) (*models.Comment, error)
|
||||
}
|
||||
|
||||
type commentRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCommentRepository(db *gorm.DB) CommentRepository {
|
||||
return &commentRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *commentRepository) Create(comment *models.Comment) error {
|
||||
return r.db.Create(comment).Error
|
||||
}
|
||||
|
||||
func (r *commentRepository) GetByNewsID(newsID uint) ([]models.Comment, error) {
|
||||
var comments []models.Comment
|
||||
err := r.db.Preload("Author").Where("news_id = ?", newsID).
|
||||
Order("created_at ASC").Find(&comments).Error
|
||||
return comments, err
|
||||
}
|
||||
|
||||
func (r *commentRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.Comment{}, id).Error
|
||||
}
|
||||
|
||||
func (r *commentRepository) GetByID(id uint) (*models.Comment, error) {
|
||||
var comment models.Comment
|
||||
err := r.db.Preload("Author").Where("id = ?", id).First(&comment).Error
|
||||
return &comment, err
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// repository/email_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EmailRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewEmailRepository(db *gorm.DB) *EmailRepository {
|
||||
return &EmailRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *EmailRepository) CreateVerificationToken(verification *models.EmailVerification) error {
|
||||
return r.db.Create(verification).Error
|
||||
}
|
||||
|
||||
func (r *EmailRepository) GetVerificationToken(token string) (*models.EmailVerification, error) {
|
||||
var verification models.EmailVerification
|
||||
err := r.db.Where("token = ? AND used = ? AND expires_at > ?", token, false, time.Now()).
|
||||
Preload("User").
|
||||
First(&verification).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &verification, nil
|
||||
}
|
||||
|
||||
func (r *EmailRepository) MarkTokenAsUsed(token string) error {
|
||||
return r.db.Model(&models.EmailVerification{}).
|
||||
Where("token = ?", token).
|
||||
Updates(map[string]interface{}{
|
||||
"used": true,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (r *EmailRepository) DeleteExpiredTokens() error {
|
||||
return r.db.Where("expires_at < ?", time.Now()).Delete(&models.EmailVerification{}).Error
|
||||
}
|
||||
|
||||
func (r *EmailRepository) GetUsersWithNewsletter() ([]models.User, error) {
|
||||
var users []models.User
|
||||
err := r.db.Where("newsletter = ? AND email_verified = ?", true, true).
|
||||
Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
// MarkEmailAsVerified помечает email пользователя как верифицированный
|
||||
func (r *EmailRepository) MarkEmailAsVerified(userID uint) error {
|
||||
return r.db.Model(&models.User{}).
|
||||
Where("id = ?", userID).
|
||||
Updates(map[string]interface{}{
|
||||
"email_verified": true,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// GetUserByEmail возвращает пользователя по email
|
||||
func (r *EmailRepository) GetUserByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdatePassword обновляет пароль пользователя
|
||||
func (r *EmailRepository) UpdatePassword(userID uint, newPassword string) error {
|
||||
return r.db.Model(&models.User{}).
|
||||
Where("id = ?", userID).
|
||||
Updates(map[string]interface{}{
|
||||
"password": newPassword,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// repository/event_registration_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EventRegistrationRepository interface {
|
||||
Create(registration *models.EventRegistration) error
|
||||
FindByID(id uint) (*models.EventRegistration, error)
|
||||
FindByEventID(eventID uint) ([]models.EventRegistration, error)
|
||||
FindByUserID(userID uint) ([]models.EventRegistration, error)
|
||||
FindByEventAndUser(eventID, userID uint) (*models.EventRegistration, error)
|
||||
Update(registration *models.EventRegistration) error
|
||||
Delete(id uint) error
|
||||
UpdateStatus(registrationID uint, status string) error
|
||||
UpdateResultTime(registrationID uint, resultTime string) error
|
||||
CountByEventID(eventID uint) (int64, error)
|
||||
}
|
||||
|
||||
type eventRegistrationRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewEventRegistrationRepository(db *gorm.DB) EventRegistrationRepository {
|
||||
return &eventRegistrationRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) Create(registration *models.EventRegistration) error {
|
||||
return r.db.Create(registration).Error
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) FindByID(id uint) (*models.EventRegistration, error) {
|
||||
var registration models.EventRegistration
|
||||
err := r.db.Preload("Event").Preload("User").First(®istration, id).Error
|
||||
return ®istration, err
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) FindByEventID(eventID uint) ([]models.EventRegistration, error) {
|
||||
var registrations []models.EventRegistration
|
||||
err := r.db.Preload("User").Where("event_id = ?", eventID).Find(®istrations).Error
|
||||
return registrations, err
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) FindByUserID(userID uint) ([]models.EventRegistration, error) {
|
||||
var registrations []models.EventRegistration
|
||||
err := r.db.Preload("Event").Where("user_id = ?", userID).Find(®istrations).Error
|
||||
return registrations, err
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) FindByEventAndUser(eventID, userID uint) (*models.EventRegistration, error) {
|
||||
var registration models.EventRegistration
|
||||
err := r.db.Where("event_id = ? AND user_id = ?", eventID, userID).First(®istration).Error
|
||||
return ®istration, err
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) Update(registration *models.EventRegistration) error {
|
||||
return r.db.Save(registration).Error
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.EventRegistration{}, id).Error
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) UpdateStatus(registrationID uint, status string) error {
|
||||
result := r.db.Model(&models.EventRegistration{}).Where("id = ?", registrationID).Update("status", status)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("registration not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) UpdateResultTime(registrationID uint, resultTime string) error {
|
||||
result := r.db.Model(&models.EventRegistration{}).Where("id = ?", registrationID).Update("result_time", resultTime)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("registration not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *eventRegistrationRepository) CountByEventID(eventID uint) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.EventRegistration{}).Where("event_id = ? AND status IN ?", eventID, []string{"pending", "confirmed"}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// repository/event_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EventRepository interface {
|
||||
Create(event *models.Event) error
|
||||
FindByID(id uint) (*models.Event, error)
|
||||
FindAll() ([]models.Event, error)
|
||||
Update(event *models.Event) error
|
||||
Delete(id uint) error
|
||||
FindByType(eventType models.EventType) ([]models.Event, error)
|
||||
FindUpcoming() ([]models.Event, error)
|
||||
FindByDateRange(startDate, endDate time.Time) ([]models.Event, error)
|
||||
UpdateParticipantsCount(eventID uint, count int) error
|
||||
UpdateRegistrationStatus(eventID uint, registrationOpen bool) error
|
||||
}
|
||||
|
||||
type eventRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewEventRepository(db *gorm.DB) EventRepository {
|
||||
return &eventRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *eventRepository) Create(event *models.Event) error {
|
||||
return r.db.Create(event).Error
|
||||
}
|
||||
|
||||
func (r *eventRepository) FindByID(id uint) (*models.Event, error) {
|
||||
var event models.Event
|
||||
err := r.db.Preload("Registrations").First(&event, id).Error
|
||||
return &event, err
|
||||
}
|
||||
|
||||
func (r *eventRepository) FindAll() ([]models.Event, error) {
|
||||
var events []models.Event
|
||||
err := r.db.Order("date DESC").Find(&events).Error
|
||||
return events, err
|
||||
}
|
||||
|
||||
func (r *eventRepository) Update(event *models.Event) error {
|
||||
return r.db.Save(event).Error
|
||||
}
|
||||
|
||||
func (r *eventRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.Event{}, id).Error
|
||||
}
|
||||
|
||||
func (r *eventRepository) FindByType(eventType models.EventType) ([]models.Event, error) {
|
||||
var events []models.Event
|
||||
err := r.db.Where("type = ?", eventType).Order("date DESC").Find(&events).Error
|
||||
return events, err
|
||||
}
|
||||
|
||||
func (r *eventRepository) FindUpcoming() ([]models.Event, error) {
|
||||
var events []models.Event
|
||||
err := r.db.Where("date >= ?", time.Now()).Order("date ASC").Find(&events).Error
|
||||
return events, err
|
||||
}
|
||||
|
||||
func (r *eventRepository) FindByDateRange(startDate, endDate time.Time) ([]models.Event, error) {
|
||||
var events []models.Event
|
||||
err := r.db.Where("date BETWEEN ? AND ?", startDate, endDate).Order("date ASC").Find(&events).Error
|
||||
return events, err
|
||||
}
|
||||
|
||||
func (r *eventRepository) UpdateParticipantsCount(eventID uint, count int) error {
|
||||
result := r.db.Model(&models.Event{}).Where("id = ?", eventID).Update("participants_count", count)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("event not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *eventRepository) UpdateRegistrationStatus(eventID uint, registrationOpen bool) error {
|
||||
result := r.db.Model(&models.Event{}).Where("id = ?", eventID).Update("registration_open", registrationOpen)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("event not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// repository/gallery_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type GalleryRepository interface {
|
||||
Create(gallery *models.Gallery) error
|
||||
FindByID(id uint) (*models.Gallery, error)
|
||||
FindAll() ([]models.Gallery, error)
|
||||
Update(gallery *models.Gallery) error
|
||||
Delete(id uint) error
|
||||
FindByCategory(category models.GalleryCategory) ([]models.Gallery, error)
|
||||
FindByAuthor(authorID uint) ([]models.Gallery, error)
|
||||
FindPopular(limit int) ([]models.Gallery, error)
|
||||
FindRecent(limit int) ([]models.Gallery, error)
|
||||
IncrementViews(galleryID uint) error
|
||||
IncrementLikes(galleryID uint) error
|
||||
DecrementLikes(galleryID uint) error
|
||||
FindByEventDateRange(startDate, endDate time.Time) ([]models.Gallery, error)
|
||||
}
|
||||
|
||||
type galleryRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewGalleryRepository(db *gorm.DB) GalleryRepository {
|
||||
return &galleryRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *galleryRepository) Create(gallery *models.Gallery) error {
|
||||
return r.db.Create(gallery).Error
|
||||
}
|
||||
|
||||
func (r *galleryRepository) FindByID(id uint) (*models.Gallery, error) {
|
||||
var gallery models.Gallery
|
||||
err := r.db.Preload("Author").First(&gallery, id).Error
|
||||
return &gallery, err
|
||||
}
|
||||
|
||||
func (r *galleryRepository) FindAll() ([]models.Gallery, error) {
|
||||
var galleries []models.Gallery
|
||||
err := r.db.Preload("Author").Order("created_at DESC").Find(&galleries).Error
|
||||
return galleries, err
|
||||
}
|
||||
|
||||
func (r *galleryRepository) Update(gallery *models.Gallery) error {
|
||||
return r.db.Save(gallery).Error
|
||||
}
|
||||
|
||||
func (r *galleryRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.Gallery{}, id).Error
|
||||
}
|
||||
|
||||
func (r *galleryRepository) FindByCategory(category models.GalleryCategory) ([]models.Gallery, error) {
|
||||
var galleries []models.Gallery
|
||||
err := r.db.Preload("Author").Where("category = ?", category).Order("created_at DESC").Find(&galleries).Error
|
||||
return galleries, err
|
||||
}
|
||||
|
||||
func (r *galleryRepository) FindByAuthor(authorID uint) ([]models.Gallery, error) {
|
||||
var galleries []models.Gallery
|
||||
err := r.db.Preload("Author").Where("author_id = ?", authorID).Order("created_at DESC").Find(&galleries).Error
|
||||
return galleries, err
|
||||
}
|
||||
|
||||
func (r *galleryRepository) FindPopular(limit int) ([]models.Gallery, error) {
|
||||
var galleries []models.Gallery
|
||||
err := r.db.Preload("Author").Order("views DESC, likes DESC").Limit(limit).Find(&galleries).Error
|
||||
return galleries, err
|
||||
}
|
||||
|
||||
func (r *galleryRepository) FindRecent(limit int) ([]models.Gallery, error) {
|
||||
var galleries []models.Gallery
|
||||
err := r.db.Preload("Author").Order("created_at DESC").Limit(limit).Find(&galleries).Error
|
||||
return galleries, err
|
||||
}
|
||||
|
||||
func (r *galleryRepository) IncrementViews(galleryID uint) error {
|
||||
result := r.db.Model(&models.Gallery{}).Where("id = ?", galleryID).Update("views", gorm.Expr("views + ?", 1))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("gallery not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *galleryRepository) IncrementLikes(galleryID uint) error {
|
||||
result := r.db.Model(&models.Gallery{}).Where("id = ?", galleryID).Update("likes", gorm.Expr("likes + ?", 1))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("gallery not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *galleryRepository) DecrementLikes(galleryID uint) error {
|
||||
result := r.db.Model(&models.Gallery{}).Where("id = ? AND likes > 0", galleryID).Update("likes", gorm.Expr("likes - ?", 1))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("gallery not found or likes already zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *galleryRepository) FindByEventDateRange(startDate, endDate time.Time) ([]models.Gallery, error) {
|
||||
var galleries []models.Gallery
|
||||
err := r.db.Preload("Author").
|
||||
Where("event_date BETWEEN ? AND ?", startDate, endDate).
|
||||
Order("event_date DESC").
|
||||
Find(&galleries).Error
|
||||
return galleries, err
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type NewsRepository interface {
|
||||
Create(news *models.News) error
|
||||
GetByID(id uint) (*models.News, error)
|
||||
GetAll(limit, offset int, category string) ([]models.News, int64, error)
|
||||
Update(news *models.News) error
|
||||
Delete(id uint) error
|
||||
IncrementViews(id uint) error
|
||||
GetByAuthor(authorID uint, limit, offset int) ([]models.News, int64, error)
|
||||
}
|
||||
|
||||
type newsRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewNewsRepository(db *gorm.DB) NewsRepository {
|
||||
return &newsRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *newsRepository) Create(news *models.News) error {
|
||||
return r.db.Create(news).Error
|
||||
}
|
||||
|
||||
func (r *newsRepository) GetByID(id uint) (*models.News, error) {
|
||||
var news models.News
|
||||
err := r.db.Preload("Author").Preload("Comments.Author").
|
||||
Where("id = ?", id).First(&news).Error
|
||||
return &news, err
|
||||
}
|
||||
|
||||
func (r *newsRepository) GetAll(limit, offset int, category string) ([]models.News, int64, error) {
|
||||
var news []models.News
|
||||
var total int64
|
||||
|
||||
query := r.db.Preload("Author")
|
||||
|
||||
if category != "" && category != "all" {
|
||||
query = query.Where("category = ?", category)
|
||||
}
|
||||
|
||||
// Получаем общее количество
|
||||
if err := query.Model(&models.News{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Получаем данные с пагинацией
|
||||
err := query.Order("created_at DESC").
|
||||
Limit(limit).Offset(offset).
|
||||
Find(&news).Error
|
||||
|
||||
return news, total, err
|
||||
}
|
||||
|
||||
func (r *newsRepository) Update(news *models.News) error {
|
||||
return r.db.Save(news).Error
|
||||
}
|
||||
|
||||
func (r *newsRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.News{}, id).Error
|
||||
}
|
||||
|
||||
func (r *newsRepository) IncrementViews(id uint) error {
|
||||
return r.db.Model(&models.News{}).Where("id = ?", id).
|
||||
Update("views", gorm.Expr("views + ?", 1)).Error
|
||||
}
|
||||
|
||||
func (r *newsRepository) GetByAuthor(authorID uint, limit, offset int) ([]models.News, int64, error) {
|
||||
var news []models.News
|
||||
var total int64
|
||||
|
||||
query := r.db.Preload("Author").Where("author_id = ?", authorID)
|
||||
|
||||
if err := query.Model(&models.News{}).Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
err := query.Order("created_at DESC").
|
||||
Limit(limit).Offset(offset).
|
||||
Find(&news).Error
|
||||
|
||||
return news, total, err
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
// repositories/personal_best_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/pkg/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PersonalBestRepository interface {
|
||||
Create(personalBest *models.PersonalBest) error
|
||||
GetByID(id uint) (*models.PersonalBest, error)
|
||||
GetByUserID(userID uint) ([]models.PersonalBest, error)
|
||||
GetByUserAndDistance(userID uint, distanceType models.DistanceType) ([]models.PersonalBest, error)
|
||||
GetBestByDistance(userID uint, distanceType models.DistanceType) (*models.PersonalBest, error)
|
||||
Update(personalBest *models.PersonalBest) error
|
||||
Delete(id uint) error
|
||||
GetVerifiedByUserID(userID uint) ([]models.PersonalBest, error)
|
||||
GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.PersonalBest, error)
|
||||
GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error)
|
||||
ExistsBetterTime(userID uint, distanceType models.DistanceType, time string) (bool, error)
|
||||
CalculatePace(timeStr string, distanceType models.DistanceType) (string, error)
|
||||
GetRecentPersonalBests(userID uint, limit int) ([]models.PersonalBest, error)
|
||||
GetByEventName(userID uint, eventName string) ([]models.PersonalBest, error)
|
||||
}
|
||||
|
||||
type personalBestRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPersonalBestRepository(db *gorm.DB) PersonalBestRepository {
|
||||
return &personalBestRepository{db: db}
|
||||
}
|
||||
|
||||
// Create создает новый личный рекорд
|
||||
func (r *personalBestRepository) Create(personalBest *models.PersonalBest) error {
|
||||
return r.db.Create(personalBest).Error
|
||||
}
|
||||
|
||||
// GetByID возвращает личный рекорд по ID
|
||||
func (r *personalBestRepository) GetByID(id uint) (*models.PersonalBest, error) {
|
||||
var personalBest models.PersonalBest
|
||||
err := r.db.Preload("User").First(&personalBest, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &personalBest, nil
|
||||
}
|
||||
|
||||
// GetByUserID возвращает все личные рекорды пользователя
|
||||
func (r *personalBestRepository) GetByUserID(userID uint) ([]models.PersonalBest, error) {
|
||||
var personalBests []models.PersonalBest
|
||||
err := r.db.Where("user_id = ?", userID).
|
||||
Preload("User").
|
||||
Order("distance_type, time").
|
||||
Find(&personalBests).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return personalBests, nil
|
||||
}
|
||||
|
||||
// GetByUserAndDistance возвращает личные рекорды пользователя по дистанции
|
||||
func (r *personalBestRepository) GetByUserAndDistance(userID uint, distanceType models.DistanceType) ([]models.PersonalBest, error) {
|
||||
var personalBests []models.PersonalBest
|
||||
err := r.db.Where("user_id = ? AND distance_type = ?", userID, distanceType).
|
||||
Preload("User").
|
||||
Order("time").
|
||||
Find(&personalBests).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return personalBests, nil
|
||||
}
|
||||
|
||||
// GetBestByDistance возвращает лучший результат пользователя на дистанции
|
||||
func (r *personalBestRepository) GetBestByDistance(userID uint, distanceType models.DistanceType) (*models.PersonalBest, error) {
|
||||
var personalBest models.PersonalBest
|
||||
err := r.db.Where("user_id = ? AND distance_type = ?", userID, distanceType).
|
||||
Preload("User").
|
||||
Order("time").
|
||||
First(&personalBest).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &personalBest, nil
|
||||
}
|
||||
|
||||
// Update обновляет личный рекорд
|
||||
func (r *personalBestRepository) Update(personalBest *models.PersonalBest) error {
|
||||
return r.db.Save(personalBest).Error
|
||||
}
|
||||
|
||||
// Delete удаляет личный рекорд
|
||||
func (r *personalBestRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.PersonalBest{}, id).Error
|
||||
}
|
||||
|
||||
// GetVerifiedByUserID возвращает подтвержденные личные рекорды пользователя
|
||||
func (r *personalBestRepository) GetVerifiedByUserID(userID uint) ([]models.PersonalBest, error) {
|
||||
var personalBests []models.PersonalBest
|
||||
err := r.db.Where("user_id = ? AND verified = ?", userID, true).
|
||||
Preload("User").
|
||||
Order("distance_type, time").
|
||||
Find(&personalBests).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return personalBests, nil
|
||||
}
|
||||
|
||||
// GetByDateRange возвращает личные рекорды за период времени
|
||||
func (r *personalBestRepository) GetByDateRange(userID uint, startDate, endDate time.Time) ([]models.PersonalBest, error) {
|
||||
var personalBests []models.PersonalBest
|
||||
err := r.db.Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate).
|
||||
Preload("User").
|
||||
Order("date DESC, distance_type").
|
||||
Find(&personalBests).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return personalBests, nil
|
||||
}
|
||||
|
||||
// GetRecentPersonalBests возвращает последние личные рекорды
|
||||
func (r *personalBestRepository) GetRecentPersonalBests(userID uint, limit int) ([]models.PersonalBest, error) {
|
||||
var personalBests []models.PersonalBest
|
||||
err := r.db.Where("user_id = ?", userID).
|
||||
Preload("User").
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Find(&personalBests).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return personalBests, nil
|
||||
}
|
||||
|
||||
// GetByEventName возвращает личные рекорды по названию события
|
||||
func (r *personalBestRepository) GetByEventName(userID uint, eventName string) ([]models.PersonalBest, error) {
|
||||
var personalBests []models.PersonalBest
|
||||
err := r.db.Where("user_id = ? AND event_name LIKE ?", userID, "%"+eventName+"%").
|
||||
Preload("User").
|
||||
Order("date DESC").
|
||||
Find(&personalBests).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return personalBests, nil
|
||||
}
|
||||
|
||||
// GetPersonalBestsSummary возвращает сводку лучших результатов по дистанциям
|
||||
// GetPersonalBestsSummary возвращает сводку лучших результатов по дистанциям
|
||||
func (r *personalBestRepository) GetPersonalBestsSummary(userID uint) (*models.PersonalBestsSummary, error) {
|
||||
summary := &models.PersonalBestsSummary{}
|
||||
|
||||
// Получаем лучший результат для каждой дистанции
|
||||
distances := []models.DistanceType{
|
||||
models.Distance5K,
|
||||
models.Distance10K,
|
||||
models.DistanceHalf,
|
||||
models.DistanceFull,
|
||||
}
|
||||
|
||||
for _, distance := range distances {
|
||||
best, err := r.GetBestByDistance(userID, distance)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
if best != nil {
|
||||
switch distance {
|
||||
case models.Distance5K:
|
||||
summary.Best5K = best.Time
|
||||
summary.Best5KPace = best.Pace
|
||||
case models.Distance10K:
|
||||
summary.Best10K = best.Time
|
||||
summary.Best10KPace = best.Pace
|
||||
case models.DistanceHalf:
|
||||
summary.BestHalf = best.Time
|
||||
summary.BestHalfPace = best.Pace
|
||||
case models.DistanceFull:
|
||||
summary.BestMarathon = best.Time
|
||||
summary.BestMarathonPace = best.Pace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
// ExistsBetterTime проверяет, есть ли у пользователя уже лучший результат на этой дистанции
|
||||
func (r *personalBestRepository) ExistsBetterTime(userID uint, distanceType models.DistanceType, time string) (bool, error) {
|
||||
var count int64
|
||||
err := r.db.Model(&models.PersonalBest{}).
|
||||
Where("user_id = ? AND distance_type = ? AND time < ?", userID, distanceType, time).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// CalculatePace вычисляет темп на основе времени и дистанции
|
||||
func (r *personalBestRepository) CalculatePace(timeStr string, distanceType models.DistanceType) (string, error) {
|
||||
// Парсим время из строки "HH:MM:SS"
|
||||
t, err := time.Parse("15:04:05", timeStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Преобразуем в секунды
|
||||
totalSeconds := t.Hour()*3600 + t.Minute()*60 + t.Second()
|
||||
|
||||
// Определяем дистанцию в метрах
|
||||
var distanceMeters float64
|
||||
switch distanceType {
|
||||
case models.Distance5K:
|
||||
distanceMeters = 5000
|
||||
case models.Distance10K:
|
||||
distanceMeters = 10000
|
||||
case models.DistanceHalf:
|
||||
distanceMeters = 21097.5 // 21.0975 km
|
||||
case models.DistanceFull:
|
||||
distanceMeters = 42195 // 42.195 km
|
||||
default:
|
||||
return "", nil // Для других дистанций не вычисляем темп
|
||||
}
|
||||
|
||||
// Вычисляем темп в секундах на километр
|
||||
paceSecondsPerKm := float64(totalSeconds) / (distanceMeters / 1000)
|
||||
|
||||
// Форматируем темп в "MM:SS"
|
||||
minutes := int(paceSecondsPerKm) / 60
|
||||
seconds := int(paceSecondsPerKm) % 60
|
||||
|
||||
return utils.FormatPace(minutes, seconds), nil
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
// repository/review_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"database/sql"
|
||||
"math"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ReviewRepository interface {
|
||||
Create(review *models.Review) error
|
||||
GetByID(id uint) (*models.Review, error)
|
||||
GetAll(page, limit int, sortBy, filter string) ([]models.Review, int64, error)
|
||||
GetByAuthorID(authorID uint) ([]models.Review, error)
|
||||
Update(review *models.Review) error
|
||||
Delete(id uint) error
|
||||
GetStats() (*models.ReviewsStatsResponse, error)
|
||||
GetRatingDistribution() (map[int]int, error)
|
||||
}
|
||||
|
||||
type reviewRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewReviewRepository(db *gorm.DB) ReviewRepository {
|
||||
return &reviewRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *reviewRepository) Create(review *models.Review) error {
|
||||
return r.db.Create(review).Error
|
||||
}
|
||||
|
||||
func (r *reviewRepository) GetByID(id uint) (*models.Review, error) {
|
||||
var review models.Review
|
||||
err := r.db.Preload("Author").First(&review, id).Error
|
||||
return &review, err
|
||||
}
|
||||
|
||||
func (r *reviewRepository) GetAll(page, limit int, sortBy, filter string) ([]models.Review, int64, error) {
|
||||
var reviews []models.Review
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&models.Review{}).Preload("Author")
|
||||
|
||||
// Применяем фильтрацию по рейтингу
|
||||
if filter != "" && filter != "all" {
|
||||
query = query.Where("rating >= ?", filter)
|
||||
}
|
||||
|
||||
// Считаем общее количество
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Применяем сортировку
|
||||
switch sortBy {
|
||||
case "newest":
|
||||
query = query.Order("created_at DESC")
|
||||
case "oldest":
|
||||
query = query.Order("created_at ASC")
|
||||
case "highest":
|
||||
query = query.Order("rating DESC, created_at DESC")
|
||||
case "lowest":
|
||||
query = query.Order("rating ASC, created_at DESC")
|
||||
default:
|
||||
query = query.Order("created_at DESC")
|
||||
}
|
||||
|
||||
// Применяем пагинацию
|
||||
offset := (page - 1) * limit
|
||||
err := query.Offset(offset).Limit(limit).Find(&reviews).Error
|
||||
|
||||
return reviews, total, err
|
||||
}
|
||||
|
||||
func (r *reviewRepository) GetByAuthorID(authorID uint) ([]models.Review, error) {
|
||||
var reviews []models.Review
|
||||
err := r.db.Where("author_id = ?", authorID).Preload("Author").Find(&reviews).Error
|
||||
return reviews, err
|
||||
}
|
||||
|
||||
func (r *reviewRepository) Update(review *models.Review) error {
|
||||
return r.db.Save(review).Error
|
||||
}
|
||||
|
||||
func (r *reviewRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.Review{}, id).Error
|
||||
}
|
||||
|
||||
func (r *reviewRepository) GetStats() (*models.ReviewsStatsResponse, error) {
|
||||
var totalReviews int64
|
||||
var averageRating float64
|
||||
var successStories int64
|
||||
|
||||
// Общее количество отзывов
|
||||
if err := r.db.Model(&models.Review{}).Count(&totalReviews).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Средний рейтинг - ИСПРАВЛЕННАЯ ЧАСТЬ
|
||||
var nullRating sql.NullFloat64
|
||||
if err := r.db.Model(&models.Review{}).Select("AVG(rating)").Row().Scan(&nullRating); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if nullRating.Valid {
|
||||
averageRating = nullRating.Float64
|
||||
} else {
|
||||
averageRating = 0
|
||||
}
|
||||
|
||||
// Количество успешных историй (отзывы с рейтингом >= 4 и достижениями)
|
||||
if err := r.db.Model(&models.Review{}).
|
||||
Where("rating >= ? AND achievement != ?", 4, "").
|
||||
Count(&successStories).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Распределение по рейтингам
|
||||
ratingDistribution, err := r.GetRatingDistribution()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.ReviewsStatsResponse{
|
||||
TotalReviews: int(totalReviews),
|
||||
AverageRating: math.Round(averageRating*100) / 100, // Округляем до 2 знаков
|
||||
SuccessStories: int(successStories),
|
||||
RatingDistribution: ratingDistribution,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *reviewRepository) GetRatingDistribution() (map[int]int, error) {
|
||||
var results []struct {
|
||||
Rating int
|
||||
Count int
|
||||
}
|
||||
|
||||
err := r.db.Model(&models.Review{}).
|
||||
Select("rating, COUNT(*) as count").
|
||||
Group("rating").
|
||||
Order("rating DESC").
|
||||
Scan(&results).Error
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
distribution := make(map[int]int)
|
||||
for _, result := range results {
|
||||
distribution[result.Rating] = result.Count
|
||||
}
|
||||
|
||||
// Заполняем отсутствующие рейтинги нулями
|
||||
for i := 1; i <= 5; i++ {
|
||||
if _, exists := distribution[i]; !exists {
|
||||
distribution[i] = 0
|
||||
}
|
||||
}
|
||||
|
||||
return distribution, nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// repositories/training_plan_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TrainingPlanRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewTrainingPlanRepository(db *gorm.DB) *TrainingPlanRepository {
|
||||
return &TrainingPlanRepository{db: db}
|
||||
}
|
||||
|
||||
// Create создает новый план тренировок
|
||||
func (r *TrainingPlanRepository) Create(plan *models.TrainingPlan) error {
|
||||
return r.db.Create(plan).Error
|
||||
}
|
||||
|
||||
// GetByID возвращает план тренировок по ID
|
||||
func (r *TrainingPlanRepository) GetByID(id uint) (*models.TrainingPlan, error) {
|
||||
var plan models.TrainingPlan
|
||||
err := r.db.Preload("Workouts").First(&plan, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &plan, nil
|
||||
}
|
||||
|
||||
// GetByUserID возвращает все планы тренировок пользователя
|
||||
func (r *TrainingPlanRepository) GetByUserID(userID uint) ([]models.TrainingPlan, error) {
|
||||
var plans []models.TrainingPlan
|
||||
err := r.db.Preload("Workouts").Where("user_id = ?", userID).Find(&plans).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plans, nil
|
||||
}
|
||||
|
||||
// Update обновляет план тренировок
|
||||
func (r *TrainingPlanRepository) Update(plan *models.TrainingPlan) error {
|
||||
return r.db.Save(plan).Error
|
||||
}
|
||||
|
||||
// Delete удаляет план тренировок
|
||||
func (r *TrainingPlanRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.TrainingPlan{}, id).Error
|
||||
}
|
||||
|
||||
// UpdateCurrentWeek обновляет текущую неделю плана тренировок
|
||||
func (r *TrainingPlanRepository) UpdateCurrentWeek(id uint, currentWeek int) error {
|
||||
return r.db.Model(&models.TrainingPlan{}).Where("id = ?", id).Update("current_week", currentWeek).Error
|
||||
}
|
||||
|
||||
// MarkAsCompleted помечает план тренировок как завершенный
|
||||
func (r *TrainingPlanRepository) MarkAsCompleted(id uint) error {
|
||||
return r.db.Model(&models.TrainingPlan{}).Where("id = ?", id).Update("completed", true).Error
|
||||
}
|
||||
|
||||
// GetActivePlan возвращает активный (не завершенный) план тренировок пользователя
|
||||
func (r *TrainingPlanRepository) GetActivePlan(userID uint) (*models.TrainingPlan, error) {
|
||||
var plan models.TrainingPlan
|
||||
err := r.db.Preload("Workouts").Where("user_id = ? AND completed = ?", userID, false).First(&plan).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &plan, nil
|
||||
}
|
||||
|
||||
// CreateWorkout создает тренировку в плане
|
||||
func (r *TrainingPlanRepository) CreateWorkout(workout *models.TrainingWorkout) error {
|
||||
return r.db.Create(workout).Error
|
||||
}
|
||||
|
||||
// GetWorkoutByID возвращает тренировку по ID
|
||||
func (r *TrainingPlanRepository) GetWorkoutByID(id uint) (*models.TrainingWorkout, error) {
|
||||
var workout models.TrainingWorkout
|
||||
err := r.db.First(&workout, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &workout, nil
|
||||
}
|
||||
|
||||
// GetWorkoutsByPlanID возвращает все тренировки плана
|
||||
func (r *TrainingPlanRepository) GetWorkoutsByPlanID(planID uint) ([]models.TrainingWorkout, error) {
|
||||
var workouts []models.TrainingWorkout
|
||||
err := r.db.Where("plan_id = ?", planID).Find(&workouts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
// UpdateWorkout обновляет тренировку
|
||||
func (r *TrainingPlanRepository) UpdateWorkout(workout *models.TrainingWorkout) error {
|
||||
return r.db.Save(workout).Error
|
||||
}
|
||||
|
||||
// DeleteWorkout удаляет тренировку
|
||||
func (r *TrainingPlanRepository) DeleteWorkout(id uint) error {
|
||||
return r.db.Delete(&models.TrainingWorkout{}, id).Error
|
||||
}
|
||||
|
||||
// MarkWorkoutAsCompleted помечает тренировку как завершенную
|
||||
func (r *TrainingPlanRepository) MarkWorkoutAsCompleted(id uint) error {
|
||||
now := time.Now()
|
||||
return r.db.Model(&models.TrainingWorkout{}).Where("id = ?", id).Updates(map[string]interface{}{
|
||||
"completed": true,
|
||||
"completed_at": now,
|
||||
}).Error
|
||||
}
|
||||
|
||||
// GetWorkoutsByWeek возвращает тренировки для определенной недели плана
|
||||
func (r *TrainingPlanRepository) GetWorkoutsByWeek(planID uint, week int) ([]models.TrainingWorkout, error) {
|
||||
var workouts []models.TrainingWorkout
|
||||
err := r.db.Where("plan_id = ? AND week = ?", planID, week).Find(&workouts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
// GetCompletedWorkouts возвращает завершенные тренировки плана
|
||||
func (r *TrainingPlanRepository) GetCompletedWorkouts(planID uint) ([]models.TrainingWorkout, error) {
|
||||
var workouts []models.TrainingWorkout
|
||||
err := r.db.Where("plan_id = ? AND completed = ?", planID, true).Find(&workouts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workouts, nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"fmt"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
Create(user *models.User) error
|
||||
FindByID(id uint) (*models.User, error)
|
||||
FindByEmail(email string) (*models.User, error)
|
||||
Update(user *models.User) error
|
||||
Delete(id uint) error
|
||||
UpdateExcludeEmail(userUpdate *models.User) error
|
||||
UpdateAvatar(userID uint, avatarPath string) error
|
||||
FindAll() ([]models.User, error)
|
||||
MarkEmailAsVerified(userID uint) error
|
||||
UpdatePassword(userID uint, newPassword string) error
|
||||
GetUserByID(id uint) (*models.User, error)
|
||||
}
|
||||
|
||||
func (r *userRepository) UpdateAvatar(userID uint, avatarPath string) error {
|
||||
result := r.db.Model(&models.User{}).Where("id = ?", userID).Update("avatar", avatarPath)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type userRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
||||
return &userRepository{db: db}
|
||||
}
|
||||
|
||||
// Add to userRepository implementation
|
||||
func (r *userRepository) FindAll() ([]models.User, error) {
|
||||
var users []models.User
|
||||
err := r.db.Find(&users).Error
|
||||
return users, err
|
||||
}
|
||||
|
||||
func (r *userRepository) Create(user *models.User) error {
|
||||
return r.db.Create(user).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByID(id uint) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.First(&user, id).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByEmail(email string) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.Where("email = ?", email).First(&user).Error
|
||||
return &user, err
|
||||
}
|
||||
|
||||
func (r *userRepository) Update(user *models.User) error {
|
||||
return r.db.Save(user).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.User{}, id).Error
|
||||
}
|
||||
|
||||
// repository/user_repository.go
|
||||
func (r *userRepository) UpdateExcludeEmail(userUpdate *models.User) error {
|
||||
result := r.db.Model(userUpdate).Where("id = ?", userUpdate.ID).Updates(map[string]interface{}{
|
||||
"first_name": userUpdate.FirstName,
|
||||
"last_name": userUpdate.LastName,
|
||||
"avatar": userUpdate.Avatar, // Добавить обновление аватара
|
||||
"phone": userUpdate.Phone,
|
||||
"experience": userUpdate.Experience,
|
||||
"goals": userUpdate.Goals,
|
||||
"newsletter": userUpdate.Newsletter,
|
||||
"updated_at": userUpdate.UpdatedAt,
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkEmailAsVerified помечает email пользователя как верифицированный
|
||||
func (r userRepository) MarkEmailAsVerified(userID uint) error {
|
||||
result := r.db.Model(&models.User{}).Where("id = ?", userID).Update("email_verified", true)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePassword обновляет пароль пользователя
|
||||
func (r userRepository) UpdatePassword(userID uint, newPassword string) error {
|
||||
result := r.db.Model(&models.User{}).Where("id = ?", userID).Update("password", newPassword)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r userRepository) GetUserByID(id uint) (*models.User, error) {
|
||||
var user models.User
|
||||
err := r.db.First(&user, id).Error
|
||||
return &user, err
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// repositories/user_stats_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"api_bb/internal/models"
|
||||
"api_bb/pkg/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserStatsRepository interface {
|
||||
Create(userStats *models.UserStats) error
|
||||
GetByID(id uint) (*models.UserStats, error)
|
||||
GetByUserID(userID uint) (*models.UserStats, error)
|
||||
Update(userStats *models.UserStats) error
|
||||
Delete(id uint) error
|
||||
UpdateStreaks(userID uint, lastWorkout time.Time) error
|
||||
UpdateWeeklyDistance(userID uint, distance float64) error
|
||||
UpdateMonthlyDistance(userID uint, distance float64) error
|
||||
IncrementWorkouts(userID uint, distance float64, duration int) error
|
||||
UpdatePersonalBest(userID uint, distanceType string, time string) error
|
||||
GetUserStatsResponse(userID uint) (*models.UserStatsResponse, error)
|
||||
GetByUserIDOrCreate(userID uint) (*models.UserStats, error)
|
||||
}
|
||||
|
||||
type userStatsRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserStatsRepository(db *gorm.DB) UserStatsRepository {
|
||||
return &userStatsRepository{db: db}
|
||||
}
|
||||
|
||||
// GetByUserIDOrCreate возвращает статистику по ID пользователя или создает новую
|
||||
func (r *userStatsRepository) GetByUserIDOrCreate(userID uint) (*models.UserStats, error) {
|
||||
userStats, err := r.GetByUserID(userID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// Создаем новую статистику
|
||||
newStats := &models.UserStats{
|
||||
UserID: userID,
|
||||
TotalDistance: 0,
|
||||
TotalTime: 0,
|
||||
AvgPace: "0:00",
|
||||
WorkoutsCount: 0,
|
||||
CurrentStreak: 0,
|
||||
LongestStreak: 0,
|
||||
WeeklyDistance: 0,
|
||||
MonthlyDistance: 0,
|
||||
Best5K: "",
|
||||
Best10K: "",
|
||||
BestHalf: "",
|
||||
BestMarathon: "",
|
||||
LastWorkout: time.Time{},
|
||||
}
|
||||
|
||||
if err := r.Create(newStats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newStats, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return userStats, nil
|
||||
}
|
||||
|
||||
// Create создает новую статистику пользователя
|
||||
func (r *userStatsRepository) Create(userStats *models.UserStats) error {
|
||||
return r.db.Create(userStats).Error
|
||||
}
|
||||
|
||||
// GetByID возвращает статистику по ID
|
||||
func (r *userStatsRepository) GetByID(id uint) (*models.UserStats, error) {
|
||||
var userStats models.UserStats
|
||||
err := r.db.First(&userStats, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userStats, nil
|
||||
}
|
||||
|
||||
// GetByUserID возвращает статистику по ID пользователя
|
||||
func (r *userStatsRepository) GetByUserID(userID uint) (*models.UserStats, error) {
|
||||
var userStats models.UserStats
|
||||
err := r.db.Where("user_id = ?", userID).First(&userStats).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &userStats, nil
|
||||
}
|
||||
|
||||
// Update обновляет статистику пользователя
|
||||
func (r *userStatsRepository) Update(userStats *models.UserStats) error {
|
||||
return r.db.Save(userStats).Error
|
||||
}
|
||||
|
||||
// Delete удаляет статистику пользователя
|
||||
func (r *userStatsRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.UserStats{}, id).Error
|
||||
}
|
||||
|
||||
// UpdateStreaks обновляет серии тренировок
|
||||
func (r *userStatsRepository) UpdateStreaks(userID uint, lastWorkout time.Time) error {
|
||||
userStats, err := r.GetByUserID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Проверяем, была ли тренировка вчера
|
||||
yesterday := time.Now().AddDate(0, 0, -1)
|
||||
if userStats.LastWorkout.Format("2006-01-02") == yesterday.Format("2006-01-02") {
|
||||
// Продолжаем серию
|
||||
userStats.CurrentStreak++
|
||||
} else if userStats.LastWorkout.Format("2006-01-02") != time.Now().Format("2006-01-02") {
|
||||
// Сбрасываем серию, если не было тренировки сегодня или вчера
|
||||
userStats.CurrentStreak = 1
|
||||
}
|
||||
|
||||
// Обновляем самую длинную серию
|
||||
if userStats.CurrentStreak > userStats.LongestStreak {
|
||||
userStats.LongestStreak = userStats.CurrentStreak
|
||||
}
|
||||
|
||||
userStats.LastWorkout = lastWorkout
|
||||
|
||||
return r.Update(userStats)
|
||||
}
|
||||
|
||||
// UpdateWeeklyDistance обновляет недельный пробег
|
||||
func (r *userStatsRepository) UpdateWeeklyDistance(userID uint, distance float64) error {
|
||||
return r.db.Model(&models.UserStats{}).
|
||||
Where("user_id = ?", userID).
|
||||
Update("weekly_distance", gorm.Expr("weekly_distance + ?", distance)).
|
||||
Error
|
||||
}
|
||||
|
||||
// UpdateMonthlyDistance обновляет месячный пробег
|
||||
func (r *userStatsRepository) UpdateMonthlyDistance(userID uint, distance float64) error {
|
||||
return r.db.Model(&models.UserStats{}).
|
||||
Where("user_id = ?", userID).
|
||||
Update("monthly_distance", gorm.Expr("monthly_distance + ?", distance)).
|
||||
Error
|
||||
}
|
||||
|
||||
// IncrementWorkouts увеличивает счетчик тренировок и обновляет общие показатели
|
||||
func (r *userStatsRepository) IncrementWorkouts(userID uint, distance float64, duration int) error {
|
||||
userStats, err := r.GetByUserID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Обновляем общие показатели
|
||||
userStats.WorkoutsCount++
|
||||
userStats.TotalDistance += distance
|
||||
userStats.TotalTime += duration
|
||||
|
||||
// Пересчитываем средний темп (в минутах на км)
|
||||
if userStats.TotalDistance > 0 {
|
||||
avgPaceMinPerKm := float64(userStats.TotalTime) / userStats.TotalDistance
|
||||
minutes := int(avgPaceMinPerKm)
|
||||
seconds := int((avgPaceMinPerKm - float64(minutes)) * 60)
|
||||
userStats.AvgPace = utils.FormatPace(minutes, seconds)
|
||||
}
|
||||
|
||||
return r.Update(userStats)
|
||||
}
|
||||
|
||||
// UpdatePersonalBest обновляет личный рекорд
|
||||
func (r *userStatsRepository) UpdatePersonalBest(userID uint, distanceType string, time string) error {
|
||||
updateField := ""
|
||||
switch distanceType {
|
||||
case "5k":
|
||||
updateField = "best_5k"
|
||||
case "10k":
|
||||
updateField = "best_10k"
|
||||
case "half":
|
||||
updateField = "best_half"
|
||||
case "marathon":
|
||||
updateField = "best_marathon"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.db.Model(&models.UserStats{}).
|
||||
Where("user_id = ?", userID).
|
||||
Update(updateField, time).
|
||||
Error
|
||||
}
|
||||
|
||||
// GetUserStatsResponse возвращает статистику в формате DTO
|
||||
func (r *userStatsRepository) GetUserStatsResponse(userID uint) (*models.UserStatsResponse, error) {
|
||||
userStats, err := r.GetByUserIDOrCreate(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.UserStatsResponse{
|
||||
TotalDistance: userStats.TotalDistance,
|
||||
TotalTime: userStats.TotalTime,
|
||||
AvgPace: userStats.AvgPace,
|
||||
WorkoutsCount: userStats.WorkoutsCount,
|
||||
CurrentStreak: userStats.CurrentStreak,
|
||||
LongestStreak: userStats.LongestStreak,
|
||||
WeeklyDistance: userStats.WeeklyDistance,
|
||||
MonthlyDistance: userStats.MonthlyDistance,
|
||||
PersonalBests: models.PersonalBestsSummary{
|
||||
Best5K: userStats.Best5K,
|
||||
Best10K: userStats.Best10K,
|
||||
BestHalf: userStats.BestHalf,
|
||||
BestMarathon: userStats.BestMarathon,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// repositories/workout_repository.go
|
||||
package repository
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type WorkoutRepository interface {
|
||||
Create(workout *models.Workout) error
|
||||
FindByID(id uint) (*models.Workout, error)
|
||||
FindByUserID(userID uint) ([]models.Workout, error)
|
||||
Update(workout *models.Workout) error
|
||||
Delete(id uint) error
|
||||
GetWorkoutStats(userID uint) (*models.WorkoutStatsResponse, error)
|
||||
FindByDateRange(userID uint, startDate, endDate time.Time) ([]models.Workout, error)
|
||||
GetMonthlyStats(userID uint, year int) ([]models.MonthlyStat, error)
|
||||
GetLatestWorkouts(userID uint, limit int) ([]models.Workout, error)
|
||||
GetByType(userID uint, workoutType models.WorkoutType) ([]models.Workout, error)
|
||||
}
|
||||
|
||||
type workoutRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("record not found")
|
||||
)
|
||||
|
||||
func NewWorkoutRepository(db *gorm.DB) WorkoutRepository {
|
||||
return &workoutRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *workoutRepository) Create(workout *models.Workout) error {
|
||||
return r.db.Create(workout).Error
|
||||
}
|
||||
|
||||
func (r *workoutRepository) FindByID(id uint) (*models.Workout, error) {
|
||||
var workout models.Workout
|
||||
err := r.db.Preload("User").First(&workout, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &workout, nil
|
||||
}
|
||||
|
||||
func (r *workoutRepository) FindByUserID(userID uint) ([]models.Workout, error) {
|
||||
var workouts []models.Workout
|
||||
err := r.db.Preload("User").Where("user_id = ?", userID).Order("date DESC").Find(&workouts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
func (r *workoutRepository) Update(workout *models.Workout) error {
|
||||
return r.db.Save(workout).Error
|
||||
}
|
||||
|
||||
func (r *workoutRepository) Delete(id uint) error {
|
||||
return r.db.Delete(&models.Workout{}, id).Error
|
||||
}
|
||||
|
||||
func (r *workoutRepository) GetWorkoutStats(userID uint) (*models.WorkoutStatsResponse, error) {
|
||||
var stats models.WorkoutStatsResponse
|
||||
|
||||
// Получаем общее количество тренировок
|
||||
var totalWorkouts int64
|
||||
if err := r.db.Model(&models.Workout{}).Where("user_id = ?", userID).Count(&totalWorkouts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.TotalWorkouts = int(totalWorkouts)
|
||||
|
||||
// Получаем общую дистанцию
|
||||
var totalDistance struct{ Total float64 }
|
||||
if err := r.db.Model(&models.Workout{}).Where("user_id = ?", userID).Select("COALESCE(SUM(distance_km), 0) as total").Scan(&totalDistance).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.TotalDistance = totalDistance.Total
|
||||
|
||||
// Получаем общее время
|
||||
var totalTime struct{ Total int }
|
||||
if err := r.db.Model(&models.Workout{}).Where("user_id = ?", userID).Select("COALESCE(SUM(duration_min), 0) as total").Scan(&totalTime).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.TotalTime = totalTime.Total
|
||||
|
||||
// Получаем месячную статистику
|
||||
monthlyStats, err := r.GetMonthlyStats(userID, time.Now().Year())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.MonthlyStats = monthlyStats
|
||||
|
||||
// Рассчитываем средний темп (упрощенная версия)
|
||||
if totalDistance.Total > 0 && totalTime.Total > 0 {
|
||||
avgPaceMinPerKm := float64(totalTime.Total) / totalDistance.Total
|
||||
minutes := int(avgPaceMinPerKm)
|
||||
seconds := int((avgPaceMinPerKm - float64(minutes)) * 60)
|
||||
stats.AveragePace = fmt.Sprintf("%d:%02d", minutes, seconds)
|
||||
} else {
|
||||
stats.AveragePace = "0:00"
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
func (r *workoutRepository) FindByDateRange(userID uint, startDate, endDate time.Time) ([]models.Workout, error) {
|
||||
var workouts []models.Workout
|
||||
err := r.db.Preload("User").
|
||||
Where("user_id = ? AND date BETWEEN ? AND ?", userID, startDate, endDate).
|
||||
Order("date DESC").
|
||||
Find(&workouts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
func (r *workoutRepository) GetMonthlyStats(userID uint, year int) ([]models.MonthlyStat, error) {
|
||||
var monthlyStats []models.MonthlyStat
|
||||
|
||||
query := `
|
||||
SELECT
|
||||
TO_CHAR(date, 'YYYY-MM') as month,
|
||||
COALESCE(SUM(distance_km), 0) as distance,
|
||||
COUNT(*) as workouts
|
||||
FROM workouts
|
||||
WHERE user_id = ? AND EXTRACT(YEAR FROM date) = ?
|
||||
GROUP BY TO_CHAR(date, 'YYYY-MM')
|
||||
ORDER BY month DESC
|
||||
`
|
||||
|
||||
err := r.db.Raw(query, userID, year).Scan(&monthlyStats).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return monthlyStats, nil
|
||||
}
|
||||
|
||||
func (r *workoutRepository) GetLatestWorkouts(userID uint, limit int) ([]models.Workout, error) {
|
||||
var workouts []models.Workout
|
||||
err := r.db.Preload("User").
|
||||
Where("user_id = ?", userID).
|
||||
Order("date DESC").
|
||||
Limit(limit).
|
||||
Find(&workouts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workouts, nil
|
||||
}
|
||||
|
||||
func (r *workoutRepository) GetByType(userID uint, workoutType models.WorkoutType) ([]models.Workout, error) {
|
||||
var workouts []models.Workout
|
||||
err := r.db.Preload("User").
|
||||
Where("user_id = ? AND type = ?", userID, workoutType).
|
||||
Order("date DESC").
|
||||
Find(&workouts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return workouts, nil
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// routes/routes.go
|
||||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"api_bb/internal/config"
|
||||
"api_bb/internal/handlers"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/internal/service"
|
||||
"api_bb/pkg/logger"
|
||||
"api_bb/pkg/middleware"
|
||||
)
|
||||
|
||||
func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Apply common middleware
|
||||
for _, m := range middleware.CommonMiddleware() {
|
||||
r.Use(m)
|
||||
}
|
||||
|
||||
// handler
|
||||
h := handlers.NewHandler(db, config)
|
||||
|
||||
// Serve static files (avatars)
|
||||
r.Handle("/uploads/*", http.StripPrefix("/uploads/",
|
||||
http.FileServer(http.Dir("./uploads"))))
|
||||
|
||||
// Initialize repositories
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
|
||||
// Initialize logger
|
||||
baseLogger := logger.NewWrapper(logger.Get())
|
||||
|
||||
// Initialize services with logger
|
||||
jwtService := service.NewJWTService(config.JWTSecret)
|
||||
|
||||
// Email service initialization with fallback
|
||||
var emailHandler *handlers.EmailHandler
|
||||
if h.EmailHandler() != nil {
|
||||
emailHandler = h.EmailHandler()
|
||||
}
|
||||
|
||||
// Health routes
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Get("/health", h.HealthHandler().HealthCheck)
|
||||
r.Get("/check", h.HealthHandler().Check)
|
||||
})
|
||||
|
||||
// API v1 routes
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
|
||||
// Email verification (public) - только если доступен
|
||||
if emailHandler != nil {
|
||||
r.Get("/verify-email", emailHandler.VerifyEmail)
|
||||
}
|
||||
|
||||
// Public auth routes
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Post("/register", h.AuthHandler().Register)
|
||||
r.Post("/login", h.AuthHandler().Login)
|
||||
r.Post("/logout", h.AuthHandler().Logout)
|
||||
|
||||
// Email routes (only if email handler is available)
|
||||
if emailHandler != nil {
|
||||
r.Post("/verify-email/resend", emailHandler.ResendVerification)
|
||||
r.Post("/password-reset/request", emailHandler.RequestPasswordReset)
|
||||
r.Post("/password-reset/confirm", emailHandler.ConfirmPasswordReset)
|
||||
}
|
||||
})
|
||||
|
||||
// Публичные маршруты для достижений (если нужны)
|
||||
r.Route("/achievements", func(r chi.Router) {
|
||||
// Публичные маршруты для просмотра достижений других пользователей
|
||||
r.Get("/user/{userID}", h.UserAchievementHandler().GetPublicUserAchievements)
|
||||
r.Get("/user/{userID}/summary", h.UserAchievementHandler().GetPublicUserAchievementsSummary)
|
||||
r.Get("/user/{userID}/recent", h.UserAchievementHandler().GetPublicRecentAchievements)
|
||||
})
|
||||
|
||||
// Protected routes
|
||||
r.Route("/user", func(r chi.Router) {
|
||||
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
|
||||
r.Use(middleware.RequireAuth)
|
||||
|
||||
// user profile routes
|
||||
r.Get("/profile", h.UserHandler().GetProfile)
|
||||
r.Post("/editProfile", h.UserHandler().UpdateProfile)
|
||||
r.Get("/", h.UserHandler().GetUsers)
|
||||
|
||||
// Все операции с аватарами теперь через AvatarHandler
|
||||
r.Route("/avatars", func(r chi.Router) {
|
||||
r.Post("/upload", h.AvatarHandler().UploadAvatar)
|
||||
r.Delete("/delete", h.AvatarHandler().DeleteAvatar)
|
||||
r.Get("/{filename}", h.AvatarHandler().GetAvatar)
|
||||
})
|
||||
|
||||
r.Route("/stats", func(r chi.Router) {
|
||||
r.Get("/", h.UserStatsHandler().GetUserStats)
|
||||
r.Get("/{userID}", h.UserStatsHandler().GetUserStatsByID)
|
||||
r.Post("/workout", h.UserStatsHandler().IncrementWorkout)
|
||||
r.Put("/personal-best", h.UserStatsHandler().UpdatePersonalBest)
|
||||
r.Post("/weekly/reset", h.UserStatsHandler().ResetWeeklyDistance)
|
||||
r.Post("/monthly/reset", h.UserStatsHandler().ResetMonthlyDistance)
|
||||
})
|
||||
|
||||
// Маршруты для тренировок
|
||||
r.Route("/workouts", func(r chi.Router) {
|
||||
r.Post("/", h.UserWorkoutHandler().CreateWorkout)
|
||||
r.Get("/", h.UserWorkoutHandler().GetWorkouts)
|
||||
r.Get("/stats", h.UserWorkoutHandler().GetWorkoutStats)
|
||||
r.Get("/type/{type}", h.UserWorkoutHandler().GetWorkoutsByType)
|
||||
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
r.Get("/", h.UserWorkoutHandler().GetWorkoutByID)
|
||||
r.Put("/", h.UserWorkoutHandler().UpdateWorkout)
|
||||
r.Delete("/", h.UserWorkoutHandler().DeleteWorkout)
|
||||
})
|
||||
})
|
||||
|
||||
// Маршруты для достижений (achievements)
|
||||
r.Route("/achievements", func(r chi.Router) {
|
||||
// Создание нового достижения
|
||||
r.Post("/", h.UserAchievementHandler().CreateAchievement)
|
||||
|
||||
// Получение всех достижений пользователя
|
||||
r.Get("/", h.UserAchievementHandler().GetUserAchievements)
|
||||
|
||||
// Получение сводки по достижениям
|
||||
r.Get("/summary", h.UserAchievementHandler().GetUserAchievementsSummary)
|
||||
|
||||
// Получение последних достижений (с опциональным лимитом)
|
||||
r.Get("/recent", h.UserAchievementHandler().GetRecentAchievements)
|
||||
|
||||
// Получение достижений по типу
|
||||
r.Get("/type/{type}", h.UserAchievementHandler().GetAchievementsByType)
|
||||
|
||||
// Операции с конкретным достижением
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
// Получение достижения по ID
|
||||
r.Get("/", h.UserAchievementHandler().GetAchievementByID)
|
||||
|
||||
// Обновление достижения
|
||||
r.Put("/", h.UserAchievementHandler().UpdateAchievement)
|
||||
|
||||
// Удаление достижения
|
||||
r.Delete("/", h.UserAchievementHandler().DeleteAchievement)
|
||||
|
||||
// Подтверждение достижения
|
||||
r.Patch("/verify", h.UserAchievementHandler().VerifyAchievement)
|
||||
})
|
||||
})
|
||||
// Personal Best routes
|
||||
r.Route("/personal-bests", func(r chi.Router) {
|
||||
// CRUD operations
|
||||
r.Post("/", h.PersonalBestHandler().CreatePersonalBest)
|
||||
r.Get("/", h.PersonalBestHandler().GetUserPersonalBests)
|
||||
r.Get("/recent", h.PersonalBestHandler().GetRecentPersonalBests)
|
||||
r.Get("/summary", h.PersonalBestHandler().GetPersonalBestsSummary)
|
||||
r.Post("/calculate-pace", h.PersonalBestHandler().CalculatePace)
|
||||
|
||||
// Distance-specific routes
|
||||
r.Route("/distance/{distanceType}", func(r chi.Router) {
|
||||
r.Get("/", h.PersonalBestHandler().GetPersonalBestsByDistance)
|
||||
r.Get("/best", h.PersonalBestHandler().GetBestByDistance)
|
||||
})
|
||||
|
||||
// Individual personal best routes
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
r.Get("/", h.PersonalBestHandler().GetPersonalBest)
|
||||
r.Put("/", h.PersonalBestHandler().UpdatePersonalBest)
|
||||
r.Delete("/", h.PersonalBestHandler().DeletePersonalBest)
|
||||
r.Patch("/verify", h.PersonalBestHandler().VerifyPersonalBest)
|
||||
})
|
||||
})
|
||||
|
||||
// Маршруты для тренировочных планов (Training Plans)
|
||||
r.Route("/training-plans", func(r chi.Router) {
|
||||
// Создание нового тренировочного плана
|
||||
r.Post("/", h.TrainingPlanHandler().CreateTrainingPlan)
|
||||
|
||||
// Получение всех тренировочных планов пользователя
|
||||
r.Get("/", h.TrainingPlanHandler().GetTrainingPlans)
|
||||
|
||||
// Получение активного тренировочного плана
|
||||
r.Get("/active", h.TrainingPlanHandler().GetActiveTrainingPlan)
|
||||
|
||||
// Обновление текущей недели плана
|
||||
r.Patch("/current-week", h.TrainingPlanHandler().UpdateCurrentWeek)
|
||||
|
||||
// Операции с конкретным тренировочным планом
|
||||
r.Route("/{id}", func(r chi.Router) {
|
||||
// Получение тренировочного плана по ID
|
||||
r.Get("/", h.TrainingPlanHandler().GetTrainingPlanByID)
|
||||
|
||||
// Обновление тренировочного плана
|
||||
r.Put("/", h.TrainingPlanHandler().UpdateTrainingPlan)
|
||||
|
||||
// Удаление тренировочного плана
|
||||
r.Delete("/", h.TrainingPlanHandler().DeleteTrainingPlan)
|
||||
|
||||
// Пометить план как завершенный
|
||||
r.Patch("/complete", h.TrainingPlanHandler().MarkTrainingPlanAsCompleted)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
r.Route("/news", func(r chi.Router) {
|
||||
|
||||
// Публичные маршруты
|
||||
r.Get("/", h.NewsHandler().GetNews)
|
||||
r.Get("/{id}", h.NewsHandler().GetNewsByID)
|
||||
r.Get("/{id}/comments", h.NewsHandler().GetComments)
|
||||
r.Get("/check", h.HealthHandler().Check)
|
||||
|
||||
// Защищенные маршруты
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
|
||||
r.Use(middleware.RequireAuth)
|
||||
|
||||
// News EndPoints
|
||||
r.Post("/", h.NewsHandler().CreateNews)
|
||||
r.Put("/{id}", h.NewsHandler().UpdateNews)
|
||||
r.Delete("/{id}", h.NewsHandler().DeleteNews)
|
||||
r.Get("/my/news", h.NewsHandler().GetUserNews)
|
||||
|
||||
r.Post("/{id}/comments", h.NewsHandler().CreateComment)
|
||||
r.Delete("/comments/{commentId}", h.NewsHandler().DeleteComment)
|
||||
|
||||
r.Get("/check", h.HealthHandler().Check)
|
||||
})
|
||||
})
|
||||
|
||||
// Маршруты для отзывов
|
||||
r.Route("/reviews", func(r chi.Router) {
|
||||
// Публичные маршруты
|
||||
r.Get("/", h.ReviewHandler().GetReviews)
|
||||
r.Get("/stats", h.ReviewHandler().GetReviewsStats)
|
||||
r.Get("/{id}", h.ReviewHandler().GetReviewByID)
|
||||
|
||||
// Защищенные маршруты
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
|
||||
r.Use(middleware.RequireAuth)
|
||||
|
||||
r.Post("/", h.ReviewHandler().CreateReview)
|
||||
r.Get("/my", h.ReviewHandler().GetMyReviews)
|
||||
r.Put("/{id}", h.ReviewHandler().UpdateReview)
|
||||
r.Delete("/{id}", h.ReviewHandler().DeleteReview)
|
||||
})
|
||||
})
|
||||
|
||||
// Events
|
||||
r.Route("/events", func(r chi.Router) {
|
||||
|
||||
// Публичные маршруты
|
||||
r.Get("/", h.EventHandler().GetAllEvents)
|
||||
r.Get("/upcoming", h.EventHandler().GetUpcomingEvents)
|
||||
r.Get("/type/{type}", h.EventHandler().GetEventsByType)
|
||||
r.Get("/{id}", h.EventHandler().GetEvent)
|
||||
r.Get("/{eventId}/availability", h.EventRegistrationHandler().CheckEventAvailability)
|
||||
|
||||
// Защищенные маршруты (требуют аутентификации)
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
|
||||
r.Use(middleware.RequireAuth)
|
||||
|
||||
// Регистрации пользователя
|
||||
r.Post("/register", h.EventRegistrationHandler().RegisterForEvent)
|
||||
r.Get("/my/registrations", h.EventRegistrationHandler().GetUserRegistrations)
|
||||
r.Delete("/registrations/{id}", h.EventRegistrationHandler().CancelRegistration)
|
||||
r.Get("/registrations/{id}", h.EventRegistrationHandler().GetRegistration)
|
||||
})
|
||||
|
||||
// Админские маршруты
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.AuthMiddleware(jwtService, userRepo))
|
||||
r.Use(middleware.RequireAuth)
|
||||
r.Use(middleware.AdminMiddleware)
|
||||
|
||||
// Управление событиями
|
||||
r.Post("/", h.EventHandler().CreateEvent)
|
||||
r.Put("/{id}", h.EventHandler().UpdateEvent)
|
||||
r.Delete("/{id}", h.EventHandler().DeleteEvent)
|
||||
r.Patch("/{id}/registration-status", h.EventHandler().ToggleRegistrationStatus)
|
||||
|
||||
// Управление регистрациями
|
||||
r.Get("/{eventId}/registrations", h.EventRegistrationHandler().GetEventRegistrations)
|
||||
r.Patch("/registrations/{id}/status", h.EventRegistrationHandler().UpdateRegistrationStatus)
|
||||
r.Patch("/registrations/{id}/result-time", h.EventRegistrationHandler().UpdateResultTime)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
// Логируем все зарегистрированные маршруты
|
||||
routeLogger := logger.NewRouteLogger(baseLogger)
|
||||
routeLogger.LogRoutes(r)
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// scripts/migrate_existing_users.go
|
||||
package main
|
||||
|
||||
import (
|
||||
"api_bb/internal/models"
|
||||
"api_bb/internal/repository"
|
||||
"api_bb/pkg/logger"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func MigrateExistingUsers(db *gorm.DB) error {
|
||||
log := logger.NewWrapper(logger.Get().With(zap.String("script", "migrate_existing_users")))
|
||||
|
||||
userRepo := repository.NewUserRepository(db)
|
||||
userStatsRepo := repository.NewUserStatsRepository(db)
|
||||
|
||||
// Получаем всех пользователей
|
||||
users, err := userRepo.FindAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("starting migration for existing users",
|
||||
zap.Int("total_users", len(users)))
|
||||
|
||||
successCount := 0
|
||||
for _, user := range users {
|
||||
// Проверяем, есть ли уже статистика
|
||||
_, err := userStatsRepo.GetByUserID(user.ID)
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
// Создаем статистику
|
||||
userStats := &models.UserStats{
|
||||
UserID: user.ID,
|
||||
TotalDistance: 0,
|
||||
TotalTime: 0,
|
||||
AvgPace: "0:00",
|
||||
WorkoutsCount: 0,
|
||||
CurrentStreak: 0,
|
||||
LongestStreak: 0,
|
||||
WeeklyDistance: 0,
|
||||
MonthlyDistance: 0,
|
||||
Best5K: "",
|
||||
Best10K: "",
|
||||
BestHalf: "",
|
||||
BestMarathon: "",
|
||||
LastWorkout: user.CreatedAt, // Используем дату создания как последнюю тренировку
|
||||
}
|
||||
|
||||
if err := userStatsRepo.Create(userStats); err != nil {
|
||||
log.Error("failed to create stats for user",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
successCount++
|
||||
log.Info("created stats for user",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.String("email", user.Email))
|
||||
} else if err != nil {
|
||||
log.Error("error checking stats for user",
|
||||
zap.Uint("user_id", user.ID),
|
||||
zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("migration completed",
|
||||
zap.Int("successful_creations", successCount),
|
||||
zap.Int("total_users", len(users)))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -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