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())
|
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)
|
// Festa timing constants (all values in seconds)
|
||||||
const (
|
const (
|
||||||
festaVotingDuration = 9000 // 150 min voting window
|
festaVotingDuration = 9000 // 150 min voting window
|
||||||
@@ -125,13 +119,10 @@ func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 {
|
|||||||
}
|
}
|
||||||
return timestamps
|
return timestamps
|
||||||
}
|
}
|
||||||
if start == 0 || TimeAdjusted().Unix() > int64(start)+festaEventLifespan {
|
var err error
|
||||||
cleanupFesta(s)
|
start, err = s.server.festaService.EnsureActiveEvent(start, TimeAdjusted(), midnight.Add(24*time.Hour))
|
||||||
// Generate a new festa, starting midnight tomorrow
|
if err != nil {
|
||||||
start = uint32(midnight.Add(24 * time.Hour).Unix())
|
s.logger.Error("Failed to ensure active festa event", zap.Error(err))
|
||||||
if err := s.server.festaRepo.InsertEvent(start); err != nil {
|
|
||||||
s.logger.Error("Failed to insert festa event", zap.Error(err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
timestamps[0] = start
|
timestamps[0] = start
|
||||||
timestamps[1] = timestamps[0] + secsPerWeek
|
timestamps[1] = timestamps[0] + secsPerWeek
|
||||||
@@ -461,7 +452,7 @@ func handleMsgMhfEntryFesta(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
|
|
||||||
func handleMsgMhfChargeFesta(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfChargeFesta(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfChargeFesta)
|
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))
|
s.logger.Error("Failed to submit festa souls", zap.Error(err))
|
||||||
}
|
}
|
||||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
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.
|
// 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 {
|
func (r *FestaRepository) SubmitSouls(charID, guildID uint32, souls []uint16) error {
|
||||||
tx, err := r.db.BeginTxx(context.Background(), nil)
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -189,9 +190,6 @@ func (r *FestaRepository) SubmitSouls(charID, guildID uint32, souls []uint16) er
|
|||||||
defer func() { _ = tx.Rollback() }()
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
for i, s := range souls {
|
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 {
|
if _, err := tx.Exec(`INSERT INTO festa_submissions VALUES ($1, $2, $3, $4, now())`, charID, guildID, i, s); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1007,10 +1007,23 @@ type mockFestaRepo struct {
|
|||||||
hasClaimed bool
|
hasClaimed bool
|
||||||
prizes []Prize
|
prizes []Prize
|
||||||
prizesErr error
|
prizesErr error
|
||||||
|
|
||||||
|
cleanupErr error
|
||||||
|
cleanupCalled bool
|
||||||
|
insertErr error
|
||||||
|
insertedStart uint32
|
||||||
|
submitErr error
|
||||||
|
submittedSouls []uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockFestaRepo) CleanupAll() error { return nil }
|
func (m *mockFestaRepo) CleanupAll() error {
|
||||||
func (m *mockFestaRepo) InsertEvent(_ uint32) error { return nil }
|
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) GetFestaEvents() ([]FestaEvent, error) { return m.events, m.eventsErr }
|
||||||
func (m *mockFestaRepo) GetTeamSouls(_ string) (uint32, error) { return m.teamSouls, m.teamErr }
|
func (m *mockFestaRepo) GetTeamSouls(_ string) (uint32, error) { return m.teamSouls, m.teamErr }
|
||||||
func (m *mockFestaRepo) GetTrialsWithMonopoly() ([]FestaTrial, error) {
|
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) HasClaimedMainPrize(_ uint32) bool { return m.hasClaimed }
|
||||||
func (m *mockFestaRepo) VoteTrial(_ uint32, _ uint32) error { return nil }
|
func (m *mockFestaRepo) VoteTrial(_ uint32, _ uint32) error { return nil }
|
||||||
func (m *mockFestaRepo) RegisterGuild(_ uint32, _ string) 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) ClaimPrize(_ uint32, _ uint32) error { return nil }
|
||||||
func (m *mockFestaRepo) ListPrizes(_ uint32, _ string) ([]Prize, error) {
|
func (m *mockFestaRepo) ListPrizes(_ uint32, _ string) ([]Prize, error) {
|
||||||
return m.prizes, m.prizesErr
|
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
|
achievementService *AchievementService
|
||||||
gachaService *GachaService
|
gachaService *GachaService
|
||||||
towerService *TowerService
|
towerService *TowerService
|
||||||
|
festaService *FestaService
|
||||||
erupeConfig *cfg.Config
|
erupeConfig *cfg.Config
|
||||||
acceptConns chan net.Conn
|
acceptConns chan net.Conn
|
||||||
deleteConns chan net.Conn
|
deleteConns chan net.Conn
|
||||||
@@ -163,6 +164,7 @@ func NewServer(config *Config) *Server {
|
|||||||
s.achievementService = NewAchievementService(s.achievementRepo, s.logger)
|
s.achievementService = NewAchievementService(s.achievementRepo, s.logger)
|
||||||
s.gachaService = NewGachaService(s.gachaRepo, s.userRepo, s.charRepo, s.logger, config.ErupeConfig.GameplayOptions.MaximumNP)
|
s.gachaService = NewGachaService(s.gachaRepo, s.userRepo, s.charRepo, s.logger, config.ErupeConfig.GameplayOptions.MaximumNP)
|
||||||
s.towerService = NewTowerService(s.towerRepo, s.logger)
|
s.towerService = NewTowerService(s.towerRepo, s.logger)
|
||||||
|
s.festaService = NewFestaService(s.festaRepo, s.logger)
|
||||||
|
|
||||||
// Mezeporta
|
// Mezeporta
|
||||||
s.stages.Store("sl1Ns200p0a0u0", NewStage("sl1Ns200p0a0u0"))
|
s.stages.Store("sl1Ns200p0a0u0", NewStage("sl1Ns200p0a0u0"))
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ func ensureTowerService(s *Server) {
|
|||||||
s.towerService = NewTowerService(s.towerRepo, s.logger)
|
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.
|
// createMockSession creates a minimal Session for testing.
|
||||||
// Imported from v9.2.x-stable and adapted for main.
|
// Imported from v9.2.x-stable and adapted for main.
|
||||||
func createMockSession(charID uint32, server *Server) *Session {
|
func createMockSession(charID uint32, server *Server) *Session {
|
||||||
|
|||||||
Reference in New Issue
Block a user