mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
200 lines
5.7 KiB
Go
200 lines
5.7 KiB
Go
package services
|
||
|
||
import (
|
||
"crypto/sha256"
|
||
"encoding/binary"
|
||
"fmt"
|
||
"log"
|
||
"math/rand"
|
||
"strings"
|
||
"time"
|
||
|
||
"fotbal-club/internal/models"
|
||
"fotbal-club/pkg/email"
|
||
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
// SweepstakesService encapsulates business logic for sweepstakes
|
||
type SweepstakesService struct {
|
||
DB *gorm.DB
|
||
Email email.EmailService
|
||
}
|
||
|
||
func NewSweepstakesService(db *gorm.DB, es email.EmailService) *SweepstakesService {
|
||
return &SweepstakesService{DB: db, Email: es}
|
||
}
|
||
|
||
// StartSweepstakesScheduler periodically finalizes ended sweepstakes and picks winners
|
||
func StartSweepstakesScheduler(db *gorm.DB, es email.EmailService) {
|
||
go func() {
|
||
ticker := time.NewTicker(1 * time.Minute)
|
||
defer ticker.Stop()
|
||
for {
|
||
if err := finalizeDueSweepstakes(db, es); err != nil {
|
||
log.Printf("[sweepstakes] finalize cycle error: %v", err)
|
||
}
|
||
<-ticker.C
|
||
}
|
||
}()
|
||
}
|
||
|
||
func finalizeDueSweepstakes(db *gorm.DB, es email.EmailService) error {
|
||
// Find sweepstakes that ended and have not selected winners yet
|
||
var list []models.Sweepstake
|
||
now := time.Now()
|
||
if err := db.Where("end_at <= ? AND (winners_selected_at IS NULL) AND status <> ?", now, "finalized").Find(&list).Error; err != nil {
|
||
return err
|
||
}
|
||
if len(list) == 0 {
|
||
return nil
|
||
}
|
||
svc := NewSweepstakesService(db, es)
|
||
for _, s := range list {
|
||
if err := svc.FinalizeSweepstake(&s, ""); err != nil {
|
||
log.Printf("[sweepstakes] finalize %d failed: %v", s.ID, err)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// FinalizeSweepstake selects winners deterministically (if DrawSeed exists) and updates records.
|
||
// If seed is empty and sweepstake.DrawSeed empty, a new seed is generated from time.
|
||
func (s *SweepstakesService) FinalizeSweepstake(sw *models.Sweepstake, seed string) error {
|
||
return s.DB.Transaction(func(tx *gorm.DB) error {
|
||
// Reload for update to avoid races
|
||
var cur models.Sweepstake
|
||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&cur, sw.ID).Error; err != nil {
|
||
return err
|
||
}
|
||
if cur.WinnersSelectedAt != nil {
|
||
return nil // already finalized
|
||
}
|
||
// Load entries
|
||
var entries []models.SweepstakeEntry
|
||
if err := tx.Where("sweepstake_id = ? AND status = ?", cur.ID, "valid").Find(&entries).Error; err != nil {
|
||
return err
|
||
}
|
||
// Load prizes
|
||
var prizes []models.SweepstakePrize
|
||
if err := tx.Where("sweepstake_id = ?", cur.ID).Order("display_order ASC, id ASC").Find(&prizes).Error; err != nil {
|
||
return err
|
||
}
|
||
// Determine number of winners
|
||
nWinners := 0
|
||
for _, p := range prizes { nWinners += max(0, p.Quantity) }
|
||
if nWinners == 0 {
|
||
if cur.TotalPrizes > 0 { nWinners = cur.TotalPrizes }
|
||
}
|
||
// Cap winners to a safe maximum
|
||
if nWinners > 100 { nWinners = 100 }
|
||
if nWinners > len(entries) { nWinners = len(entries) }
|
||
|
||
// Build seed
|
||
effSeed := strings.TrimSpace(seed)
|
||
if effSeed == "" { effSeed = strings.TrimSpace(cur.DrawSeed) }
|
||
if effSeed == "" { effSeed = fmt.Sprintf("%d-%d", cur.ID, time.Now().UnixNano()) }
|
||
// Deterministic RNG from SHA-256
|
||
h := sha256.Sum256([]byte(effSeed))
|
||
base := binary.LittleEndian.Uint64(h[:8])
|
||
rng := rand.New(rand.NewSource(int64(base)))
|
||
|
||
// Shuffle indices
|
||
idx := rng.Perm(len(entries))
|
||
picked := make(map[uint]bool)
|
||
winners := make([]models.SweepstakeWinner, 0, nWinners)
|
||
assign := func(userID uint, entryID uint, prize *models.SweepstakePrize) {
|
||
w := models.SweepstakeWinner{
|
||
SweepstakeID: cur.ID,
|
||
EntryID: entryID,
|
||
UserID: userID,
|
||
ClaimStatus: "pending",
|
||
}
|
||
if prize != nil {
|
||
w.PrizeID = &prize.ID
|
||
w.PrizeName = prize.Name
|
||
}
|
||
winners = append(winners, w)
|
||
}
|
||
|
||
pos := 0
|
||
// Assign by prizes first
|
||
for i := range prizes {
|
||
q := max(0, prizes[i].Quantity)
|
||
for j := 0; j < q && pos < len(idx); j++ {
|
||
cand := entries[idx[pos]]
|
||
pos++
|
||
if picked[cand.UserID] { j--; continue }
|
||
picked[cand.UserID] = true
|
||
assign(cand.UserID, cand.ID, &prizes[i])
|
||
if len(winners) >= nWinners { break }
|
||
}
|
||
if len(winners) >= nWinners { break }
|
||
}
|
||
// If still need more (when TotalPrizes used)
|
||
for len(winners) < nWinners && pos < len(idx) {
|
||
cand := entries[idx[pos]]
|
||
pos++
|
||
if picked[cand.UserID] { continue }
|
||
picked[cand.UserID] = true
|
||
assign(cand.UserID, cand.ID, nil)
|
||
}
|
||
|
||
// Persist winners
|
||
for i := range winners {
|
||
if err := tx.Create(&winners[i]).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
now := time.Now()
|
||
vis := cur.EndAt.Add(72 * time.Hour)
|
||
if err := tx.Model(&models.Sweepstake{}).Where("id = ?", cur.ID).Updates(map[string]interface{}{
|
||
"winners_selected_at": now,
|
||
"visibility_until": vis,
|
||
"draw_seed": effSeed,
|
||
"status": "finalized",
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// Send emails (best-effort)
|
||
if s.Email != nil && len(winners) > 0 {
|
||
for _, w := range winners {
|
||
var user models.User
|
||
_ = tx.First(&user, w.UserID).Error
|
||
if strings.TrimSpace(user.Email) == "" { continue }
|
||
_ = s.Email.SendEmail(&email.EmailData{
|
||
Subject: "Vyhráli jste v soutěži!",
|
||
To: []string{strings.TrimSpace(user.Email)},
|
||
Template: "sweepstake_winner_user",
|
||
Data: map[string]interface{}{
|
||
"Title": cur.Title,
|
||
"PrizeName": w.PrizeName,
|
||
"EndsAt": cur.EndAt.Format(time.RFC1123),
|
||
},
|
||
})
|
||
}
|
||
// Admin summary
|
||
var set models.Settings
|
||
_ = tx.First(&set).Error
|
||
adminTo := strings.TrimSpace(set.ContactEmail)
|
||
if adminTo == "" { adminTo = strings.TrimSpace(set.SMTPFrom) }
|
||
if adminTo != "" {
|
||
_ = s.Email.SendEmail(&email.EmailData{
|
||
Subject: "Soutěž – vybraní výherci",
|
||
To: []string{adminTo},
|
||
Template: "sweepstake_winner_admin",
|
||
Data: map[string]interface{}{
|
||
"Title": cur.Title,
|
||
"WinnersCount": len(winners),
|
||
},
|
||
})
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
func max(a, b int) int { if a > b { return a } ; return b }
|