193 lines
6.3 KiB
Go
193 lines
6.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sno/internal/models"
|
|
"sno/internal/repository"
|
|
"sno/internal/types"
|
|
"github.com/google/uuid"
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
// AdminService defines the interface for admin-related business logic.
|
|
type AdminService interface {
|
|
GrantStars(ctx context.Context, userID int64, amount int) error
|
|
GenerateUniqueQRCodes(ctx context.Context, qrType, qrValue string, count int) ([]string, error)
|
|
GetUserRole(ctx context.Context, userID int64) (types.UserRole, error)
|
|
CreateOperator(ctx context.Context, telegramID int64, name string) error
|
|
DeleteOperator(ctx context.Context, telegramID int64) error
|
|
GetAnalytics(ctx context.Context) (*models.Analytics, error)
|
|
}
|
|
|
|
// adminService implements the AdminService interface.
|
|
type adminService struct {
|
|
userRepo repository.UserRepository
|
|
adminRepo repository.AdminRepository
|
|
redisClient *redis.Client
|
|
}
|
|
|
|
// NewAdminService creates a new instance of an admin service.
|
|
func NewAdminService(userRepo repository.UserRepository, adminRepo repository.AdminRepository, redisClient *redis.Client) AdminService {
|
|
return &adminService{
|
|
userRepo: userRepo,
|
|
adminRepo: adminRepo,
|
|
redisClient: redisClient,
|
|
}
|
|
}
|
|
|
|
// GrantStars adds or removes stars from a user's balance.
|
|
func (s *adminService) GrantStars(ctx context.Context, userID int64, amount int) error {
|
|
if amount == 0 {
|
|
return errors.New("amount cannot be zero")
|
|
}
|
|
// We might want to check if the user exists first, but the UPDATE query
|
|
// will just do nothing if the user doesn't exist, which is safe.
|
|
return s.userRepo.UpdateStarsBalance(ctx, userID, amount)
|
|
}
|
|
|
|
// GenerateUniqueQRCodes generates unique QR codes and stores them in Redis.
|
|
// The format in Redis will be a SET named `unique_qrs:<type>:<value>`
|
|
// Each member of the set will be a UUID.
|
|
func (s *adminService) GenerateUniqueQRCodes(ctx context.Context, qrType, qrValue string, count int) ([]string, error) {
|
|
if count <= 0 {
|
|
return nil, errors.New("count must be positive")
|
|
}
|
|
|
|
key := fmt.Sprintf("unique_qrs:%s:%s", qrType, qrValue)
|
|
codes := make([]string, count)
|
|
members := make([]interface{}, count)
|
|
|
|
for i := 0; i < count; i++ {
|
|
newUUID := uuid.New().String()
|
|
codes[i] = newUUID
|
|
members[i] = newUUID
|
|
}
|
|
|
|
// Add all generated UUIDs to the Redis set
|
|
if err := s.redisClient.SAdd(ctx, key, members...).Err(); err != nil {
|
|
return nil, fmt.Errorf("failed to add unique QR codes to Redis: %w", err)
|
|
}
|
|
|
|
return codes, nil
|
|
}
|
|
|
|
// GetUserRole retrieves the user's role from the admins table
|
|
func (s *adminService) GetUserRole(ctx context.Context, userID int64) (types.UserRole, error) {
|
|
return s.adminRepo.GetUserRole(ctx, userID)
|
|
}
|
|
|
|
// CreateOperator creates a new operator with the given telegram ID and name
|
|
func (s *adminService) CreateOperator(ctx context.Context, telegramID int64, name string) error {
|
|
// Check if user already exists as admin
|
|
existingAdmin, err := s.adminRepo.GetByTelegramID(ctx, telegramID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check existing admin: %w", err)
|
|
}
|
|
if existingAdmin != nil {
|
|
return errors.New("user is already an admin or operator")
|
|
}
|
|
|
|
// Create new operator
|
|
admin := &models.Admin{
|
|
TelegramID: telegramID,
|
|
Role: models.RoleOperator,
|
|
Name: &name,
|
|
}
|
|
|
|
return s.adminRepo.Create(ctx, admin)
|
|
}
|
|
|
|
// DeleteOperator deletes an operator by their telegram ID
|
|
func (s *adminService) DeleteOperator(ctx context.Context, telegramID int64) error {
|
|
// Check if user exists and is an operator
|
|
existingAdmin, err := s.adminRepo.GetByTelegramID(ctx, telegramID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check existing admin: %w", err)
|
|
}
|
|
if existingAdmin == nil {
|
|
return errors.New("operator not found")
|
|
}
|
|
if existingAdmin.Role != models.RoleOperator {
|
|
return errors.New("user is not an operator")
|
|
}
|
|
|
|
// Delete the operator
|
|
return s.adminRepo.Delete(ctx, telegramID)
|
|
}
|
|
|
|
// GetAnalytics retrieves various statistics and metrics for the admin dashboard
|
|
func (s *adminService) GetAnalytics(ctx context.Context) (*models.Analytics, error) {
|
|
analytics := &models.Analytics{}
|
|
|
|
// Query all analytics data in a single transaction for consistency
|
|
tx, err := s.userRepo.GetDB().Begin(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to begin transaction: %w", err)
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
// Total users
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM users").Scan(&analytics.TotalUsers)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get total users: %w", err)
|
|
}
|
|
|
|
// Total quizzes
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM quizzes").Scan(&analytics.TotalQuizzes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get total quizzes: %w", err)
|
|
}
|
|
|
|
// Active quizzes
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM quizzes WHERE is_active = true").Scan(&analytics.ActiveQuizzes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get active quizzes: %w", err)
|
|
}
|
|
|
|
// Total rewards
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM rewards").Scan(&analytics.TotalRewards)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get total rewards: %w", err)
|
|
}
|
|
|
|
// Active rewards
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM rewards WHERE is_active = true").Scan(&analytics.ActiveRewards)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get active rewards: %w", err)
|
|
}
|
|
|
|
// Total quiz attempts
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM quiz_attempts").Scan(&analytics.TotalAttempts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get total quiz attempts: %w", err)
|
|
}
|
|
|
|
// Total purchases
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM purchases").Scan(&analytics.TotalPurchases)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get total purchases: %w", err)
|
|
}
|
|
|
|
// Total QR scans
|
|
err = tx.QueryRow(ctx, "SELECT COUNT(*) FROM qr_scans").Scan(&analytics.TotalQRScans)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get total QR scans: %w", err)
|
|
}
|
|
|
|
// Stars distributed (from quiz attempts)
|
|
err = tx.QueryRow(ctx, "SELECT COALESCE(SUM(stars_earned), 0) FROM quiz_attempts").Scan(&analytics.StarsDistributed)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get stars distributed: %w", err)
|
|
}
|
|
|
|
// Stars spent (from purchases)
|
|
err = tx.QueryRow(ctx, "SELECT COALESCE(SUM(stars_spent), 0) FROM purchases").Scan(&analytics.StarsSpent)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get stars spent: %w", err)
|
|
}
|
|
|
|
return analytics, nil
|
|
}
|