402296b726
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
506 lines
17 KiB
Go
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,
|
|
})
|
|
} |