diff --git a/serv_nginx/api_bb/internal/database/migrate.go b/serv_nginx/api_bb/internal/database/migrate.go index 1286e37..452f029 100644 --- a/serv_nginx/api_bb/internal/database/migrate.go +++ b/serv_nginx/api_bb/internal/database/migrate.go @@ -21,6 +21,7 @@ func (d *Database) Migrate() error { &models.Review{}, &models.UserStats{}, &models.Workout{}, + &models.Achievement{}, // Добавьте другие модели здесь } @@ -81,6 +82,8 @@ func getModelName(model interface{}) string { return "Статистика Пользователя" case *models.Workout: return "Тренировки пользователя" + case *models.Achievement: + return "Достижения пользователя" default: return "Unknown" } diff --git a/serv_nginx/api_bb/internal/handlers/handlers.go b/serv_nginx/api_bb/internal/handlers/handlers.go index 215ac2f..bf943ee 100644 --- a/serv_nginx/api_bb/internal/handlers/handlers.go +++ b/serv_nginx/api_bb/internal/handlers/handlers.go @@ -19,6 +19,7 @@ type Handler struct { reviewHandler *ReviewHandler userStatsHandler *UserStatsHandler userWorkoutHandler *UserWorkoutHandler + userAchievementHandler * UserAchievementHandler // Здесь будут добавлены другие обработчики // userHandler *UserHandler // eventHandler *EventHandler @@ -33,6 +34,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { reviewRepo := repository.NewReviewRepository(db) userStatsRepo := repository.NewUserStatsRepository(db) userWorkoutRepo := repository.NewWorkoutRepository(db) + userAchievemenRepo := repository.NewAchievementRepository(db) // Initialize logger baseLogger := logger.NewWrapper(logger.Get()) // Создаем базовый логгер @@ -46,6 +48,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { reviewService := service.NewReviewService(reviewRepo, baseLogger) userStatsService := service.NewUserStatsService(userStatsRepo) userWorkoutService := service.NewWorkoutService(userWorkoutRepo) + achievementService := service.NewAchievementService(userAchievemenRepo) // Инициализация обработчиков healthHandler := NewHealthHandler() @@ -56,6 +59,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { reviewHandler := NewReviewHandler(reviewService, baseLogger) userStatsHandler := NewUserStatsHandler(userStatsService) userWorkoutHandler := NewUserWorkoutHandler(userWorkoutService) + userAchievementHandler := NewUserAchievementHandler(*achievementService) return &Handler{ healthHandler: healthHandler, @@ -66,6 +70,7 @@ func NewHandler(db *gorm.DB, cfg *config.Config) *Handler { reviewHandler: reviewHandler, userStatsHandler: userStatsHandler, userWorkoutHandler: userWorkoutHandler, + userAchievementHandler: userAchievementHandler, } } @@ -101,3 +106,7 @@ func (h *Handler) UserStatsHandler() *UserStatsHandler { func (h *Handler) UserWorkoutHandler() *UserWorkoutHandler { return h.userWorkoutHandler } + +func (h * Handler) UserAchievementHandler() *UserAchievementHandler { + return h.userAchievementHandler +} diff --git a/serv_nginx/api_bb/internal/handlers/user_achievement_handler.go b/serv_nginx/api_bb/internal/handlers/user_achievement_handler.go new file mode 100644 index 0000000..1776c59 --- /dev/null +++ b/serv_nginx/api_bb/internal/handlers/user_achievement_handler.go @@ -0,0 +1,617 @@ +// 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), + ) + if err == service.ErrAchievementNotFound { + utils.RespondWithError(w, http.StatusNotFound, "Achievement not found") + } else if err == service.ErrAchievementAlreadyExists { + utils.RespondWithError(w, http.StatusConflict, "Achievement with this title already exists") + } else { + 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", + }) +} diff --git a/serv_nginx/api_bb/internal/routes/routes.go b/serv_nginx/api_bb/internal/routes/routes.go index eb7c77c..bc2f672 100644 --- a/serv_nginx/api_bb/internal/routes/routes.go +++ b/serv_nginx/api_bb/internal/routes/routes.go @@ -26,7 +26,7 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { // handler allHandler := handlers.NewHandler(db, config) - // Serve static files (avatars) - ДОБАВЬТЕ ЭТО + // Serve static files (avatars) r.Handle("/uploads/*", http.StripPrefix("/uploads/", http.FileServer(http.Dir("./uploads")))) @@ -69,7 +69,6 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { // Все операции с аватарами теперь через AvatarHandler r.Route("/avatars", func(r chi.Router) { - r.Post("/upload", allHandler.AvatarHandler().UploadAvatar) r.Delete("/delete", allHandler.AvatarHandler().DeleteAvatar) r.Get("/{filename}", allHandler.AvatarHandler().GetAvatar) @@ -84,7 +83,7 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { r.Post("/monthly/reset", allHandler.UserStatsHandler().ResetMonthlyDistance) }) - // Маршруты для тренировок + // Маршруты для тренировок r.Route("/workouts", func(r chi.Router) { r.Post("/", allHandler.UserWorkoutHandler().CreateWorkout) r.Get("/", allHandler.UserWorkoutHandler().GetWorkouts) @@ -97,7 +96,39 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { r.Delete("/", allHandler.UserWorkoutHandler().DeleteWorkout) }) }) - // Здесь будут другие защищенные маршруты пользователя + + // Маршруты для достижений (achievements) + r.Route("/achievements", func(r chi.Router) { + // Создание нового достижения + r.Post("/", allHandler.UserAchievementHandler().CreateAchievement) + + // Получение всех достижений пользователя + r.Get("/", allHandler.UserAchievementHandler().GetUserAchievements) + + // Получение сводки по достижениям + r.Get("/summary", allHandler.UserAchievementHandler().GetUserAchievementsSummary) + + // Получение последних достижений (с опциональным лимитом) + r.Get("/recent", allHandler.UserAchievementHandler().GetRecentAchievements) + + // Получение достижений по типу + r.Get("/type/{type}", allHandler.UserAchievementHandler().GetAchievementsByType) + + // Операции с конкретным достижением + r.Route("/{id}", func(r chi.Router) { + // Получение достижения по ID + r.Get("/", allHandler.UserAchievementHandler().GetAchievementByID) + + // Обновление достижения + r.Put("/", allHandler.UserAchievementHandler().UpdateAchievement) + + // Удаление достижения + r.Delete("/", allHandler.UserAchievementHandler().DeleteAchievement) + + // Подтверждение достижения + r.Patch("/verify", allHandler.UserAchievementHandler().VerifyAchievement) + }) + }) }) r.Route("/news", func(r chi.Router) { @@ -144,9 +175,14 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { r.Delete("/{id}", allHandler.ReviewHandler().DeleteReview) }) }) - // Здесь будут добавлены другие маршруты: - // r.Mount("/events", eventHandler.Routes()) - // r.Mount("/reviews", reviewHandler.Routes()) + + // Публичные маршруты для достижений (если нужны) + r.Route("/achievements", func(r chi.Router) { + // Публичные маршруты для просмотра достижений других пользователей + r.Get("/user/{userID}", allHandler.UserAchievementHandler().GetPublicUserAchievements) + r.Get("/user/{userID}/summary", allHandler.UserAchievementHandler().GetPublicUserAchievementsSummary) + r.Get("/user/{userID}/recent", allHandler.UserAchievementHandler().GetPublicRecentAchievements) + }) }) // Логируем все зарегистрированные маршруты @@ -154,4 +190,4 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler { routeLogger.LogRoutes(r) return r -} +} \ No newline at end of file diff --git a/serv_nginx/api_bb/internal/service/achievement_service.go b/serv_nginx/api_bb/internal/service/achievement_service.go index 796b872..d7803e5 100644 --- a/serv_nginx/api_bb/internal/service/achievement_service.go +++ b/serv_nginx/api_bb/internal/service/achievement_service.go @@ -1,4 +1,4 @@ -// service/achievement_service.go +// service/achievement_service.go (дополнение) package service import ( @@ -47,6 +47,29 @@ func (s *AchievementService) CreateAchievement(userID uint, req models.Achieveme 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) @@ -97,8 +120,62 @@ func (s *AchievementService) DeleteAchievement(achievementID uint, userID uint) 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") -) \ No newline at end of file +) diff --git a/serv_nginx/api_bb/pkg/utils/validation.go b/serv_nginx/api_bb/pkg/utils/validation.go index 4e958b1..00dba50 100644 --- a/serv_nginx/api_bb/pkg/utils/validation.go +++ b/serv_nginx/api_bb/pkg/utils/validation.go @@ -359,3 +359,40 @@ func LogValidationErrors(logger *zap.Logger, err error, context string) { } } } + +// ParseUintFromQuery парсит uint из query параметра +func ParseUintFromQuery(queryParam string, defaultValue uint) (uint, error) { + if queryParam == "" { + return defaultValue, nil + } + + value, err := strconv.ParseUint(queryParam, 10, 32) + if err != nil { + return defaultValue, err + } + + return uint(value), nil +} + +// ParseIntFromQuery парсит int из query параметра +func ParseIntFromQuery(queryParam string, defaultValue int) (int, error) { + if queryParam == "" { + return defaultValue, nil + } + + value, err := strconv.Atoi(queryParam) + if err != nil { + return defaultValue, err + } + + return value, nil +} + +// ParseBoolFromQuery парсит bool из query параметра +func ParseBoolFromQuery(queryParam string, defaultValue bool) bool { + if queryParam == "" { + return defaultValue + } + + return strings.ToLower(queryParam) == "true" || queryParam == "1" +} diff --git a/serv_nginx/bbvue/src/views/Home.vue b/serv_nginx/bbvue/src/views/Home.vue index c677162..a901aaf 100644 --- a/serv_nginx/bbvue/src/views/Home.vue +++ b/serv_nginx/bbvue/src/views/Home.vue @@ -383,6 +383,7 @@ export default { .btn-primary { border: 2px solid; + background-color: #ffea00; color: #ffffff; box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3); }