238 lines
5.5 KiB
Go
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(¤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
|
|
}
|
|
|
|
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
|
|
}
|