init
This commit is contained in:
99
backend/internal/socialrating/handler.go
Normal file
99
backend/internal/socialrating/handler.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package socialrating
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"social-raiting.nekiiinkognito.ru/internal/models"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
Service Service
|
||||
}
|
||||
|
||||
type ChangeRequest struct {
|
||||
TargetUserID uint `json:"targetUserId" binding:"required"`
|
||||
Amount int `json:"amount"`
|
||||
Reason string `json:"reason"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type ChangeResponse struct {
|
||||
Operation models.SocialRatingOperation `json:"operation"`
|
||||
CurrentRating models.UserSocialRating `json:"currentRating"`
|
||||
}
|
||||
|
||||
func NewHandler(service Service) Handler {
|
||||
return Handler{Service: service}
|
||||
}
|
||||
|
||||
func (h Handler) Increase(ctx *gin.Context) {
|
||||
h.applySignedChange(ctx, "increase", 1)
|
||||
}
|
||||
|
||||
func (h Handler) Decrease(ctx *gin.Context) {
|
||||
h.applySignedChange(ctx, "decrease", -1)
|
||||
}
|
||||
|
||||
func (h Handler) applySignedChange(ctx *gin.Context, operationType string, direction int) {
|
||||
var req ChangeRequest
|
||||
if err := ctx.ShouldBindJSON(&req); err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
actorUserID, ok := getActorUserID(ctx)
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "missing user context"})
|
||||
return
|
||||
}
|
||||
|
||||
amount := req.Amount
|
||||
if amount == 0 {
|
||||
amount = 1
|
||||
}
|
||||
if amount < 0 {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "amount must be greater than zero"})
|
||||
return
|
||||
}
|
||||
|
||||
operation, currentRating, err := h.Service.ApplyChange(ChangeInput{
|
||||
TargetUserID: req.TargetUserID,
|
||||
ActorUserID: &actorUserID,
|
||||
Delta: amount * direction,
|
||||
OperationType: operationType,
|
||||
Reason: req.Reason,
|
||||
Source: req.Source,
|
||||
})
|
||||
if err != nil {
|
||||
handleApplyChangeError(ctx, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, ChangeResponse{
|
||||
Operation: operation,
|
||||
CurrentRating: currentRating,
|
||||
})
|
||||
}
|
||||
|
||||
func getActorUserID(ctx *gin.Context) (uint, bool) {
|
||||
value, exists := ctx.Get("userID")
|
||||
if !exists {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
userID, ok := value.(uint)
|
||||
return userID, ok
|
||||
}
|
||||
|
||||
func handleApplyChangeError(ctx *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, gorm.ErrRecordNotFound):
|
||||
ctx.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
default:
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
97
backend/internal/socialrating/service.go
Normal file
97
backend/internal/socialrating/service.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package socialrating
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
"social-raiting.nekiiinkognito.ru/internal/models"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
type ChangeInput struct {
|
||||
TargetUserID uint
|
||||
ActorUserID *uint
|
||||
Delta int
|
||||
OperationType string
|
||||
Reason string
|
||||
Source string
|
||||
}
|
||||
|
||||
func NewService(db *gorm.DB) Service {
|
||||
return Service{DB: db}
|
||||
}
|
||||
|
||||
func (s Service) ApplyChange(input ChangeInput) (models.SocialRatingOperation, models.UserSocialRating, error) {
|
||||
var operation models.SocialRatingOperation
|
||||
var currentRating models.UserSocialRating
|
||||
|
||||
if err := validateChangeInput(input); err != nil {
|
||||
return operation, currentRating, err
|
||||
}
|
||||
|
||||
err := s.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := ensureUserExists(tx, input.TargetUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if input.ActorUserID != nil {
|
||||
if err := ensureUserExists(tx, *input.ActorUserID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
currentRating = models.UserSocialRating{UserID: input.TargetUserID}
|
||||
if err := tx.FirstOrCreate(¤tRating, models.UserSocialRating{
|
||||
UserID: input.TargetUserID,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Social rating is allowed to go below zero, so no lower bound is applied here.
|
||||
currentRating.Score += input.Delta
|
||||
|
||||
operation = models.SocialRatingOperation{
|
||||
TargetUserID: input.TargetUserID,
|
||||
ActorUserID: input.ActorUserID,
|
||||
Delta: input.Delta,
|
||||
OperationType: strings.TrimSpace(input.OperationType),
|
||||
Reason: strings.TrimSpace(input.Reason),
|
||||
Source: strings.TrimSpace(input.Source),
|
||||
BalanceAfter: currentRating.Score,
|
||||
}
|
||||
if err := tx.Create(&operation).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
currentRating.LastOperationID = &operation.ID
|
||||
return tx.Save(¤tRating).Error
|
||||
})
|
||||
|
||||
return operation, currentRating, err
|
||||
}
|
||||
|
||||
func validateChangeInput(input ChangeInput) error {
|
||||
if input.TargetUserID == 0 {
|
||||
return errors.New("target user id is required")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(input.OperationType) == "" {
|
||||
return errors.New("operation type is required")
|
||||
}
|
||||
|
||||
if input.Delta == 0 {
|
||||
return errors.New("delta must not be zero")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureUserExists(tx *gorm.DB, userID uint) error {
|
||||
var user models.User
|
||||
return tx.First(&user, "id = ?", userID).Error
|
||||
}
|
||||
Reference in New Issue
Block a user