Files
social-rating/backend/internal/socialrating/service.go
2026-04-18 14:38:37 +03:00

238 lines
5.5 KiB
Go

package socialrating
import (
"errors"
"strconv"
"strings"
"time"
"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
}
type UserWithRating struct {
ID uint `json:"id"`
Email string `json:"email"`
IsAdmin bool `json:"isAdmin"`
Score int `json:"score"`
LastOperationID *uint `json:"lastOperationId,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
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
}
func (s Service) ListUsersWithRatings(limit int) ([]UserWithRating, error) {
if limit <= 0 {
limit = 50
}
var rows []struct {
ID uint
Email string
IsAdmin bool
Score int
LastOperationID *uint
CreatedAt time.Time
UpdatedAt time.Time
}
err := s.DB.
Table("users").
Select(
"users.id, users.email, users.is_admin, COALESCE(user_social_ratings.score, 0) AS score, user_social_ratings.last_operation_id, users.created_at, users.updated_at",
).
Joins("LEFT JOIN user_social_ratings ON user_social_ratings.user_id = users.id").
Order("score DESC, users.id ASC").
Limit(limit).
Scan(&rows).Error
if err != nil {
return nil, err
}
result := make([]UserWithRating, 0, len(rows))
for _, row := range rows {
result = append(result, UserWithRating{
ID: row.ID,
Email: row.Email,
IsAdmin: row.IsAdmin,
Score: row.Score,
LastOperationID: row.LastOperationID,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
})
}
return result, nil
}
func (s Service) GetUserRating(userID uint) (UserWithRating, error) {
var row struct {
ID uint
Email string
IsAdmin bool
Score int
LastOperationID *uint
CreatedAt time.Time
UpdatedAt time.Time
}
err := s.DB.
Table("users").
Select(
"users.id, users.email, users.is_admin, COALESCE(user_social_ratings.score, 0) AS score, user_social_ratings.last_operation_id, users.created_at, users.updated_at",
).
Joins("LEFT JOIN user_social_ratings ON user_social_ratings.user_id = users.id").
Where("users.id = ?", userID).
Take(&row).Error
if err != nil {
return UserWithRating{}, err
}
return UserWithRating{
ID: row.ID,
Email: row.Email,
IsAdmin: row.IsAdmin,
Score: row.Score,
LastOperationID: row.LastOperationID,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}, nil
}
func (s Service) GetUserHistory(userID uint, limit int) ([]models.SocialRatingOperation, error) {
if limit <= 0 {
limit = 50
}
if err := ensureUserExists(s.DB, userID); err != nil {
return nil, err
}
var operations []models.SocialRatingOperation
err := s.DB.
Where("target_user_id = ?", userID).
Order("created_at DESC, id DESC").
Limit(limit).
Find(&operations).Error
return operations, err
}
func (s Service) GetRecentOperations(limit int) ([]models.SocialRatingOperation, error) {
if limit <= 0 {
limit = 50
}
var operations []models.SocialRatingOperation
err := s.DB.
Order("created_at DESC, id DESC").
Limit(limit).
Find(&operations).Error
return operations, err
}
func parsePositiveLimit(raw string, fallback int) int {
if raw == "" {
return fallback
}
value, err := strconv.Atoi(raw)
if err != nil || value <= 0 {
return fallback
}
if value > 200 {
return 200
}
return value
}