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 }