Yimaru-BackEnd/internal/web_server/handlers/ratings.go

328 lines
9.5 KiB
Go

package handlers
import (
"Yimaru-Backend/internal/domain"
"strconv"
"github.com/gofiber/fiber/v2"
)
type submitRatingReq struct {
TargetType string `json:"target_type" validate:"required"`
TargetID int64 `json:"target_id"`
Stars int16 `json:"stars" validate:"required,min=1,max=5"`
Review *string `json:"review"`
}
type ratingRes struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
TargetType string `json:"target_type"`
TargetID int64 `json:"target_id"`
Stars int16 `json:"stars"`
Review *string `json:"review"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type ratingSummaryRes struct {
TotalCount int64 `json:"total_count"`
AverageStars float64 `json:"average_stars"`
}
func mapRatingToRes(r domain.Rating) ratingRes {
return ratingRes{
ID: r.ID,
UserID: r.UserID,
TargetType: string(r.TargetType),
TargetID: r.TargetID,
Stars: r.Stars,
Review: r.Review,
CreatedAt: r.CreatedAt.String(),
UpdatedAt: r.UpdatedAt.String(),
}
}
func isValidTargetType(t string) bool {
switch domain.RatingTargetType(t) {
case domain.RatingTargetApp, domain.RatingTargetCourse, domain.RatingTargetSubCourse:
return true
}
return false
}
// SubmitRating godoc
// @Summary Submit a rating
// @Description Submit a rating for an app, course, or sub-course
// @Tags ratings
// @Accept json
// @Produce json
// @Param body body submitRatingReq true "Submit rating payload"
// @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/ratings [post]
func (h *Handler) SubmitRating(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
var req submitRatingReq
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
if !isValidTargetType(req.TargetType) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid target_type, must be one of: app, course, sub_course",
})
}
targetType := domain.RatingTargetType(req.TargetType)
targetID := req.TargetID
if targetType == domain.RatingTargetApp {
targetID = 0
}
rating, err := h.ratingSvc.SubmitRating(c.Context(), userID, targetType, targetID, req.Stars, req.Review)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to submit rating",
Error: err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Rating submitted successfully",
Data: mapRatingToRes(rating),
})
}
// GetMyRating godoc
// @Summary Get my rating for a target
// @Description Returns the current user's rating for a specific target
// @Tags ratings
// @Produce json
// @Param target_type query string true "Target type (app, course, sub_course)"
// @Param target_id query int true "Target ID (0 for app)"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/ratings/me [get]
func (h *Handler) GetMyRating(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
targetType := c.Query("target_type")
if !isValidTargetType(targetType) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid target_type, must be one of: app, course, sub_course",
})
}
targetID, err := strconv.ParseInt(c.Query("target_id", "0"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid target_id",
Error: err.Error(),
})
}
if domain.RatingTargetType(targetType) == domain.RatingTargetApp {
targetID = 0
}
rating, err := h.ratingSvc.GetMyRating(c.Context(), userID, domain.RatingTargetType(targetType), targetID)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "Rating not found",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Rating retrieved successfully",
Data: mapRatingToRes(rating),
})
}
// GetRatingsByTarget godoc
// @Summary Get ratings for a target
// @Description Returns paginated ratings for a specific target
// @Tags ratings
// @Produce json
// @Param target_type query string true "Target type (app, course, sub_course)"
// @Param target_id query int true "Target ID (0 for app)"
// @Param limit query int false "Limit (default 20)"
// @Param offset query int false "Offset (default 0)"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/ratings [get]
func (h *Handler) GetRatingsByTarget(c *fiber.Ctx) error {
targetType := c.Query("target_type")
if !isValidTargetType(targetType) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid target_type, must be one of: app, course, sub_course",
})
}
targetID, err := strconv.ParseInt(c.Query("target_id", "0"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid target_id",
Error: err.Error(),
})
}
if domain.RatingTargetType(targetType) == domain.RatingTargetApp {
targetID = 0
}
limit, err := strconv.ParseInt(c.Query("limit", "20"), 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid limit",
Error: err.Error(),
})
}
offset, err := strconv.ParseInt(c.Query("offset", "0"), 10, 32)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid offset",
Error: err.Error(),
})
}
ratings, err := h.ratingSvc.GetRatingsByTarget(c.Context(), domain.RatingTargetType(targetType), targetID, int32(limit), int32(offset))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve ratings",
Error: err.Error(),
})
}
res := make([]ratingRes, len(ratings))
for i, r := range ratings {
res[i] = mapRatingToRes(r)
}
return c.JSON(domain.Response{
Message: "Ratings retrieved successfully",
Data: res,
})
}
// GetRatingSummary godoc
// @Summary Get rating summary for a target
// @Description Returns the total count and average stars for a specific target
// @Tags ratings
// @Produce json
// @Param target_type query string true "Target type (app, course, sub_course)"
// @Param target_id query int true "Target ID (0 for app)"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/ratings/summary [get]
func (h *Handler) GetRatingSummary(c *fiber.Ctx) error {
targetType := c.Query("target_type")
if !isValidTargetType(targetType) {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid target_type, must be one of: app, course, sub_course",
})
}
targetID, err := strconv.ParseInt(c.Query("target_id", "0"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid target_id",
Error: err.Error(),
})
}
if domain.RatingTargetType(targetType) == domain.RatingTargetApp {
targetID = 0
}
summary, err := h.ratingSvc.GetRatingSummary(c.Context(), domain.RatingTargetType(targetType), targetID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve rating summary",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Rating summary retrieved successfully",
Data: ratingSummaryRes{
TotalCount: summary.TotalCount,
AverageStars: summary.AverageStars,
},
})
}
// GetMyRatings godoc
// @Summary Get all my ratings
// @Description Returns all ratings submitted by the current user
// @Tags ratings
// @Produce json
// @Success 200 {object} domain.Response
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/ratings/me/all [get]
func (h *Handler) GetMyRatings(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
ratings, err := h.ratingSvc.GetUserRatings(c.Context(), userID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to retrieve ratings",
Error: err.Error(),
})
}
res := make([]ratingRes, len(ratings))
for i, r := range ratings {
res[i] = mapRatingToRes(r)
}
return c.JSON(domain.Response{
Message: "Ratings retrieved successfully",
Data: res,
})
}
// DeleteRating godoc
// @Summary Delete a rating
// @Description Deletes a rating by ID for the current user
// @Tags ratings
// @Produce json
// @Param id path int true "Rating ID"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/ratings/{id} [delete]
func (h *Handler) DeleteRating(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
ratingID, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid rating ID",
Error: err.Error(),
})
}
if err := h.ratingSvc.DeleteRating(c.Context(), ratingID, userID); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to delete rating",
Error: err.Error(),
})
}
return c.JSON(domain.Response{
Message: "Rating deleted successfully",
})
}