This commit is contained in:
2026-04-18 10:21:51 +03:00
commit 90d027025b
37 changed files with 6493 additions and 0 deletions

View 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()})
}
}

View 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(&currentRating, 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(&currentRating).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
}