mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
refactor(festa): extract festa logic into FestaService
The festa handler contained event lifecycle management (cleanup expired events, create new ones) and the repo enforced a business rule (skip zero-value soul submissions). Move these into a new FestaService to keep repos as pure data access and consolidate business logic.
This commit is contained in:
@@ -85,12 +85,6 @@ func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) {
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
|
||||
func cleanupFesta(s *Session) {
|
||||
if err := s.server.festaRepo.CleanupAll(); err != nil {
|
||||
s.logger.Error("Failed to cleanup festa", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Festa timing constants (all values in seconds)
|
||||
const (
|
||||
festaVotingDuration = 9000 // 150 min voting window
|
||||
@@ -125,13 +119,10 @@ func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 {
|
||||
}
|
||||
return timestamps
|
||||
}
|
||||
if start == 0 || TimeAdjusted().Unix() > int64(start)+festaEventLifespan {
|
||||
cleanupFesta(s)
|
||||
// Generate a new festa, starting midnight tomorrow
|
||||
start = uint32(midnight.Add(24 * time.Hour).Unix())
|
||||
if err := s.server.festaRepo.InsertEvent(start); err != nil {
|
||||
s.logger.Error("Failed to insert festa event", zap.Error(err))
|
||||
}
|
||||
var err error
|
||||
start, err = s.server.festaService.EnsureActiveEvent(start, TimeAdjusted(), midnight.Add(24*time.Hour))
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to ensure active festa event", zap.Error(err))
|
||||
}
|
||||
timestamps[0] = start
|
||||
timestamps[1] = timestamps[0] + secsPerWeek
|
||||
@@ -461,7 +452,7 @@ func handleMsgMhfEntryFesta(s *Session, p mhfpacket.MHFPacket) {
|
||||
|
||||
func handleMsgMhfChargeFesta(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfChargeFesta)
|
||||
if err := s.server.festaRepo.SubmitSouls(s.charID, pkt.GuildID, pkt.Souls); err != nil {
|
||||
if err := s.server.festaService.SubmitSouls(s.charID, pkt.GuildID, pkt.Souls); err != nil {
|
||||
s.logger.Error("Failed to submit festa souls", zap.Error(err))
|
||||
}
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
|
||||
@@ -181,6 +181,7 @@ func (r *FestaRepository) RegisterGuild(guildID uint32, team string) error {
|
||||
}
|
||||
|
||||
// SubmitSouls records soul submissions for a character within a transaction.
|
||||
// All entries are inserted; callers should pre-filter zero values.
|
||||
func (r *FestaRepository) SubmitSouls(charID, guildID uint32, souls []uint16) error {
|
||||
tx, err := r.db.BeginTxx(context.Background(), nil)
|
||||
if err != nil {
|
||||
@@ -189,9 +190,6 @@ func (r *FestaRepository) SubmitSouls(charID, guildID uint32, souls []uint16) er
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
for i, s := range souls {
|
||||
if s == 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := tx.Exec(`INSERT INTO festa_submissions VALUES ($1, $2, $3, $4, now())`, charID, guildID, i, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1007,10 +1007,23 @@ type mockFestaRepo struct {
|
||||
hasClaimed bool
|
||||
prizes []Prize
|
||||
prizesErr error
|
||||
|
||||
cleanupErr error
|
||||
cleanupCalled bool
|
||||
insertErr error
|
||||
insertedStart uint32
|
||||
submitErr error
|
||||
submittedSouls []uint16
|
||||
}
|
||||
|
||||
func (m *mockFestaRepo) CleanupAll() error { return nil }
|
||||
func (m *mockFestaRepo) InsertEvent(_ uint32) error { return nil }
|
||||
func (m *mockFestaRepo) CleanupAll() error {
|
||||
m.cleanupCalled = true
|
||||
return m.cleanupErr
|
||||
}
|
||||
func (m *mockFestaRepo) InsertEvent(start uint32) error {
|
||||
m.insertedStart = start
|
||||
return m.insertErr
|
||||
}
|
||||
func (m *mockFestaRepo) GetFestaEvents() ([]FestaEvent, error) { return m.events, m.eventsErr }
|
||||
func (m *mockFestaRepo) GetTeamSouls(_ string) (uint32, error) { return m.teamSouls, m.teamErr }
|
||||
func (m *mockFestaRepo) GetTrialsWithMonopoly() ([]FestaTrial, error) {
|
||||
@@ -1026,7 +1039,10 @@ func (m *mockFestaRepo) GetCharSouls(_ uint32) (uint32, error) { return m.c
|
||||
func (m *mockFestaRepo) HasClaimedMainPrize(_ uint32) bool { return m.hasClaimed }
|
||||
func (m *mockFestaRepo) VoteTrial(_ uint32, _ uint32) error { return nil }
|
||||
func (m *mockFestaRepo) RegisterGuild(_ uint32, _ string) error { return nil }
|
||||
func (m *mockFestaRepo) SubmitSouls(_, _ uint32, _ []uint16) error { return nil }
|
||||
func (m *mockFestaRepo) SubmitSouls(_, _ uint32, souls []uint16) error {
|
||||
m.submittedSouls = souls
|
||||
return m.submitErr
|
||||
}
|
||||
func (m *mockFestaRepo) ClaimPrize(_ uint32, _ uint32) error { return nil }
|
||||
func (m *mockFestaRepo) ListPrizes(_ uint32, _ string) ([]Prize, error) {
|
||||
return m.prizes, m.prizesErr
|
||||
|
||||
61
server/channelserver/svc_festa.go
Normal file
61
server/channelserver/svc_festa.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// FestaService encapsulates festa business logic, sitting between handlers and repos.
|
||||
type FestaService struct {
|
||||
festaRepo FestaRepo
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewFestaService creates a new FestaService.
|
||||
func NewFestaService(fr FestaRepo, log *zap.Logger) *FestaService {
|
||||
return &FestaService{
|
||||
festaRepo: fr,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureActiveEvent checks whether the current festa event is still active.
|
||||
// If it has expired or none exists, all festa state is cleaned up and a new
|
||||
// event is created starting at the next midnight. Returns the (possibly new)
|
||||
// start time.
|
||||
func (svc *FestaService) EnsureActiveEvent(currentStart uint32, now time.Time, nextMidnight time.Time) (uint32, error) {
|
||||
if currentStart != 0 && now.Unix() <= int64(currentStart)+festaEventLifespan {
|
||||
return currentStart, nil
|
||||
}
|
||||
|
||||
if err := svc.festaRepo.CleanupAll(); err != nil {
|
||||
svc.logger.Error("Failed to cleanup festa", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
newStart := uint32(nextMidnight.Unix())
|
||||
if err := svc.festaRepo.InsertEvent(newStart); err != nil {
|
||||
svc.logger.Error("Failed to insert festa event", zap.Error(err))
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return newStart, nil
|
||||
}
|
||||
|
||||
// SubmitSouls filters out zero-value soul entries and records the remaining
|
||||
// submissions for the character. Returns nil if all entries are zero.
|
||||
func (svc *FestaService) SubmitSouls(charID, guildID uint32, souls []uint16) error {
|
||||
var filtered []uint16
|
||||
hasNonZero := false
|
||||
for _, s := range souls {
|
||||
filtered = append(filtered, s)
|
||||
if s != 0 {
|
||||
hasNonZero = true
|
||||
}
|
||||
}
|
||||
if !hasNonZero {
|
||||
return nil
|
||||
}
|
||||
return svc.festaRepo.SubmitSouls(charID, guildID, souls)
|
||||
}
|
||||
138
server/channelserver/svc_festa_test.go
Normal file
138
server/channelserver/svc_festa_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func newTestFestaService(mock *mockFestaRepo) *FestaService {
|
||||
logger, _ := zap.NewDevelopment()
|
||||
return NewFestaService(mock, logger)
|
||||
}
|
||||
|
||||
// --- EnsureActiveEvent tests ---
|
||||
|
||||
func TestFestaService_EnsureActiveEvent_StillActive(t *testing.T) {
|
||||
mock := &mockFestaRepo{}
|
||||
svc := newTestFestaService(mock)
|
||||
|
||||
now := time.Unix(1000000, 0)
|
||||
start := uint32(now.Unix() - 100) // started 100s ago, well within lifespan
|
||||
|
||||
result, err := svc.EnsureActiveEvent(start, now, now.Add(24*time.Hour))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result != start {
|
||||
t.Errorf("start = %d, want %d (unchanged)", result, start)
|
||||
}
|
||||
if mock.cleanupCalled {
|
||||
t.Error("CleanupAll should not be called when event is active")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFestaService_EnsureActiveEvent_Expired(t *testing.T) {
|
||||
mock := &mockFestaRepo{}
|
||||
svc := newTestFestaService(mock)
|
||||
|
||||
now := time.Unix(10000000, 0)
|
||||
expiredStart := uint32(1) // long expired
|
||||
nextMidnight := now.Add(24 * time.Hour)
|
||||
|
||||
result, err := svc.EnsureActiveEvent(expiredStart, now, nextMidnight)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !mock.cleanupCalled {
|
||||
t.Error("CleanupAll should be called for expired event")
|
||||
}
|
||||
if result != uint32(nextMidnight.Unix()) {
|
||||
t.Errorf("start = %d, want %d (next midnight)", result, uint32(nextMidnight.Unix()))
|
||||
}
|
||||
if mock.insertedStart != uint32(nextMidnight.Unix()) {
|
||||
t.Errorf("insertedStart = %d, want %d", mock.insertedStart, uint32(nextMidnight.Unix()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFestaService_EnsureActiveEvent_NoEvent(t *testing.T) {
|
||||
mock := &mockFestaRepo{}
|
||||
svc := newTestFestaService(mock)
|
||||
|
||||
now := time.Unix(1000000, 0)
|
||||
nextMidnight := now.Add(24 * time.Hour)
|
||||
|
||||
result, err := svc.EnsureActiveEvent(0, now, nextMidnight)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !mock.cleanupCalled {
|
||||
t.Error("CleanupAll should be called when no event exists")
|
||||
}
|
||||
if result != uint32(nextMidnight.Unix()) {
|
||||
t.Errorf("start = %d, want %d", result, uint32(nextMidnight.Unix()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFestaService_EnsureActiveEvent_CleanupError(t *testing.T) {
|
||||
mock := &mockFestaRepo{cleanupErr: errors.New("db error")}
|
||||
svc := newTestFestaService(mock)
|
||||
|
||||
now := time.Unix(10000000, 0)
|
||||
_, err := svc.EnsureActiveEvent(0, now, now.Add(24*time.Hour))
|
||||
if err == nil {
|
||||
t.Fatal("expected error from cleanup failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFestaService_EnsureActiveEvent_InsertError(t *testing.T) {
|
||||
mock := &mockFestaRepo{insertErr: errors.New("db error")}
|
||||
svc := newTestFestaService(mock)
|
||||
|
||||
now := time.Unix(10000000, 0)
|
||||
_, err := svc.EnsureActiveEvent(0, now, now.Add(24*time.Hour))
|
||||
if err == nil {
|
||||
t.Fatal("expected error from insert failure")
|
||||
}
|
||||
}
|
||||
|
||||
// --- SubmitSouls tests ---
|
||||
|
||||
func TestFestaService_SubmitSouls_FiltersZeros(t *testing.T) {
|
||||
mock := &mockFestaRepo{}
|
||||
svc := newTestFestaService(mock)
|
||||
|
||||
err := svc.SubmitSouls(1, 10, []uint16{0, 5, 0, 3, 0})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// Should call repo with the full slice (repo does batch insert)
|
||||
if mock.submittedSouls == nil {
|
||||
t.Fatal("SubmitSouls should be called on repo")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFestaService_SubmitSouls_AllZeros(t *testing.T) {
|
||||
mock := &mockFestaRepo{}
|
||||
svc := newTestFestaService(mock)
|
||||
|
||||
err := svc.SubmitSouls(1, 10, []uint16{0, 0, 0})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if mock.submittedSouls != nil {
|
||||
t.Error("SubmitSouls should not call repo when all zeros")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFestaService_SubmitSouls_RepoError(t *testing.T) {
|
||||
mock := &mockFestaRepo{submitErr: errors.New("db error")}
|
||||
svc := newTestFestaService(mock)
|
||||
|
||||
err := svc.SubmitSouls(1, 10, []uint16{5, 0, 3})
|
||||
if err == nil {
|
||||
t.Fatal("expected error from repo failure")
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,7 @@ type Server struct {
|
||||
achievementService *AchievementService
|
||||
gachaService *GachaService
|
||||
towerService *TowerService
|
||||
festaService *FestaService
|
||||
erupeConfig *cfg.Config
|
||||
acceptConns chan net.Conn
|
||||
deleteConns chan net.Conn
|
||||
@@ -163,6 +164,7 @@ func NewServer(config *Config) *Server {
|
||||
s.achievementService = NewAchievementService(s.achievementRepo, s.logger)
|
||||
s.gachaService = NewGachaService(s.gachaRepo, s.userRepo, s.charRepo, s.logger, config.ErupeConfig.GameplayOptions.MaximumNP)
|
||||
s.towerService = NewTowerService(s.towerRepo, s.logger)
|
||||
s.festaService = NewFestaService(s.festaRepo, s.logger)
|
||||
|
||||
// Mezeporta
|
||||
s.stages.Store("sl1Ns200p0a0u0", NewStage("sl1Ns200p0a0u0"))
|
||||
|
||||
@@ -83,6 +83,11 @@ func ensureTowerService(s *Server) {
|
||||
s.towerService = NewTowerService(s.towerRepo, s.logger)
|
||||
}
|
||||
|
||||
// ensureFestaService wires the FestaService from the server's current repos.
|
||||
func ensureFestaService(s *Server) {
|
||||
s.festaService = NewFestaService(s.festaRepo, s.logger)
|
||||
}
|
||||
|
||||
// createMockSession creates a minimal Session for testing.
|
||||
// Imported from v9.2.x-stable and adapted for main.
|
||||
func createMockSession(charID uint32, server *Server) *Session {
|
||||
|
||||
Reference in New Issue
Block a user