b9f68b5dcb
renamed: "serv_nginx/api_bb/internal/handlers/training_plan_handler\321\216\320\277\321\211" -> serv_nginx/api_bb/internal/handlers/training_plan_handler.go modified: serv_nginx/api_bb/internal/handlers/user_achievement_handler.go modified: serv_nginx/api_bb/internal/routes/routes.go add routing, handlers for trainingPlan object
557 lines
19 KiB
Go
557 lines
19 KiB
Go
// 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,
|
|
}
|
|
} |