Files
tp/serv_nginx/api_bb/internal/handlers/personal_best_handler.go
T
valitovgaziz 402296b726 modified: serv_nginx/api_bb/internal/database/migrate.go
modified:   serv_nginx/api_bb/internal/handlers/handlers.go
	new file:   serv_nginx/api_bb/internal/handlers/personal_best_handler.go
	modified:   serv_nginx/api_bb/internal/models/personal_best.go
	modified:   serv_nginx/api_bb/internal/models/user_stats.go
	modified:   serv_nginx/api_bb/internal/repository/personal_best_repository.go
	modified:   serv_nginx/api_bb/internal/routes/routes.go
	modified:   serv_nginx/api_bb/internal/service/personal_best_service.go
	modified:   serv_nginx/bbvue/src/stores/user.js
personal bests add handler, rout, service, repository, logic and
migrations for
2025-10-20 03:06:06 +05:00

506 lines
17 KiB
Go

// 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,
})
}