mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
refactor(mail): extract mail logic into MailService
Introduce MailService as a convenience layer between handlers/services and MailRepo. Provides Send, SendSystem, SendGuildInvite, and BroadcastToGuild methods that encapsulate the boolean flag combinations. GuildService now depends on MailService instead of MailRepo directly, simplifying its mail-sending calls from verbose SendMail(..., false, true) to clean SendSystem(recipientID, subject, body). Guild mail broadcast logic moved from handleMsgMhfSendMail into MailService.BroadcastToGuild.
This commit is contained in:
@@ -194,31 +194,21 @@ func handleMsgMhfOprtMail(s *Session, p mhfpacket.MHFPacket) {
|
||||
func handleMsgMhfSendMail(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfSendMail)
|
||||
|
||||
if pkt.RecipientID == 0 { // Guild mail
|
||||
if pkt.RecipientID == 0 { // Guild mail broadcast
|
||||
g, err := s.server.guildRepo.GetByCharID(s.charID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get guild info for mail")
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
gm, err := s.server.guildRepo.GetMembers(g.ID, false)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get guild members for mail")
|
||||
if err := s.server.mailService.BroadcastToGuild(s.charID, g.ID, pkt.Subject, pkt.Body); err != nil {
|
||||
s.logger.Error("Failed to broadcast guild mail", zap.Error(err))
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(gm); i++ {
|
||||
err := s.server.mailRepo.SendMail(s.charID, gm[i].CharID, pkt.Subject, pkt.Body, 0, 0, false, false)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to send mail")
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err := s.server.mailRepo.SendMail(s.charID, pkt.RecipientID, pkt.Subject, pkt.Body, pkt.ItemID, pkt.Quantity, false, false)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to send mail")
|
||||
if err := s.server.mailService.Send(s.charID, pkt.RecipientID, pkt.Subject, pkt.Body, pkt.ItemID, pkt.Quantity); err != nil {
|
||||
s.logger.Error("Failed to send mail", zap.Error(err))
|
||||
}
|
||||
}
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
|
||||
@@ -386,6 +386,7 @@ func TestHandleMsgMhfSendMail_Direct(t *testing.T) {
|
||||
server := createMockServer()
|
||||
mock := &mockMailRepo{}
|
||||
server.mailRepo = mock
|
||||
ensureMailService(server)
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfSendMail{
|
||||
@@ -436,6 +437,7 @@ func TestHandleMsgMhfSendMail_Guild(t *testing.T) {
|
||||
}
|
||||
server.mailRepo = mailMock
|
||||
server.guildRepo = guildMock
|
||||
ensureMailService(server)
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfSendMail{
|
||||
@@ -470,6 +472,7 @@ func TestHandleMsgMhfSendMail_GuildNotFound(t *testing.T) {
|
||||
guildMock := &mockGuildRepoForMail{getErr: errNotFound}
|
||||
server.mailRepo = mailMock
|
||||
server.guildRepo = guildMock
|
||||
ensureMailService(server)
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfSendMail{
|
||||
|
||||
@@ -265,9 +265,10 @@ func (m *mockGoocooRepo) SaveSlot(_ uint32, slot uint32, data []byte) error {
|
||||
// --- mockGuildRepo (minimal, for SendMail guild path) ---
|
||||
|
||||
type mockGuildRepoForMail struct {
|
||||
guild *Guild
|
||||
members []*GuildMember
|
||||
getErr error
|
||||
guild *Guild
|
||||
members []*GuildMember
|
||||
getErr error
|
||||
getMembersErr error
|
||||
}
|
||||
|
||||
func (m *mockGuildRepoForMail) GetByCharID(_ uint32) (*Guild, error) {
|
||||
@@ -278,6 +279,9 @@ func (m *mockGuildRepoForMail) GetByCharID(_ uint32) (*Guild, error) {
|
||||
}
|
||||
|
||||
func (m *mockGuildRepoForMail) GetMembers(_ uint32, _ bool) ([]*GuildMember, error) {
|
||||
if m.getMembersErr != nil {
|
||||
return nil, m.getMembersErr
|
||||
}
|
||||
return m.members, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -84,16 +84,16 @@ type AnswerScoutResult struct {
|
||||
// GuildService encapsulates guild business logic, sitting between handlers and repos.
|
||||
type GuildService struct {
|
||||
guildRepo GuildRepo
|
||||
mailRepo MailRepo
|
||||
mailSvc *MailService
|
||||
charRepo CharacterRepo
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewGuildService creates a new GuildService.
|
||||
func NewGuildService(gr GuildRepo, mr MailRepo, cr CharacterRepo, log *zap.Logger) *GuildService {
|
||||
func NewGuildService(gr GuildRepo, ms *MailService, cr CharacterRepo, log *zap.Logger) *GuildService {
|
||||
return &GuildService{
|
||||
guildRepo: gr,
|
||||
mailRepo: mr,
|
||||
mailSvc: ms,
|
||||
charRepo: cr,
|
||||
logger: log,
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func (svc *GuildService) OperateMember(actorCharID, targetCharID uint32, action
|
||||
}
|
||||
|
||||
// Send mail best-effort
|
||||
if mailErr := svc.mailRepo.SendMail(mail.SenderID, mail.RecipientID, mail.Subject, mail.Body, 0, 0, false, true); mailErr != nil {
|
||||
if mailErr := svc.mailSvc.SendSystem(mail.RecipientID, mail.Subject, mail.Body); mailErr != nil {
|
||||
svc.logger.Warn("Failed to send guild member operation mail", zap.Error(mailErr))
|
||||
}
|
||||
|
||||
@@ -255,9 +255,8 @@ func (svc *GuildService) Leave(charID, guildID uint32, isApplicant bool, guildNa
|
||||
}
|
||||
|
||||
// Best-effort withdrawal notification
|
||||
if err := svc.mailRepo.SendMail(0, charID, "Withdrawal",
|
||||
fmt.Sprintf("You have withdrawn from 「%s」.", guildName),
|
||||
0, 0, false, true); err != nil {
|
||||
if err := svc.mailSvc.SendSystem(charID, "Withdrawal",
|
||||
fmt.Sprintf("You have withdrawn from 「%s」.", guildName)); err != nil {
|
||||
svc.logger.Warn("Failed to send guild withdrawal notification", zap.Error(err))
|
||||
}
|
||||
|
||||
@@ -342,7 +341,7 @@ func (svc *GuildService) AnswerScout(charID, leaderID uint32, accept bool, strin
|
||||
|
||||
// Send mails best-effort
|
||||
for _, m := range mails {
|
||||
if mailErr := svc.mailRepo.SendMail(m.SenderID, m.RecipientID, m.Subject, m.Body, 0, 0, false, true); mailErr != nil {
|
||||
if mailErr := svc.mailSvc.SendSystem(m.RecipientID, m.Subject, m.Body); mailErr != nil {
|
||||
svc.logger.Warn("Failed to send guild scout response mail", zap.Error(mailErr))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,15 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func newTestMailService(mr MailRepo, gr GuildRepo) *MailService {
|
||||
logger, _ := zap.NewDevelopment()
|
||||
return NewMailService(mr, gr, logger)
|
||||
}
|
||||
|
||||
func newTestGuildService(gr GuildRepo, mr MailRepo) *GuildService {
|
||||
logger, _ := zap.NewDevelopment()
|
||||
return NewGuildService(gr, mr, nil, logger)
|
||||
ms := newTestMailService(mr, gr)
|
||||
return NewGuildService(gr, ms, nil, logger)
|
||||
}
|
||||
|
||||
func TestGuildService_OperateMember(t *testing.T) {
|
||||
|
||||
55
server/channelserver/svc_mail.go
Normal file
55
server/channelserver/svc_mail.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// MailService encapsulates mail-sending business logic, sitting between
|
||||
// handlers/services and the MailRepo. It provides convenient methods for
|
||||
// common mail patterns (system notifications, guild broadcasts, player mail)
|
||||
// so callers don't need to specify boolean flags directly.
|
||||
type MailService struct {
|
||||
mailRepo MailRepo
|
||||
guildRepo GuildRepo
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewMailService creates a new MailService.
|
||||
func NewMailService(mr MailRepo, gr GuildRepo, log *zap.Logger) *MailService {
|
||||
return &MailService{
|
||||
mailRepo: mr,
|
||||
guildRepo: gr,
|
||||
logger: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Send sends a player-to-player mail with an optional item attachment.
|
||||
func (svc *MailService) Send(senderID, recipientID uint32, subject, body string, itemID, quantity uint16) error {
|
||||
return svc.mailRepo.SendMail(senderID, recipientID, subject, body, itemID, quantity, false, false)
|
||||
}
|
||||
|
||||
// SendSystem sends a system notification mail (no item, flagged as system message).
|
||||
func (svc *MailService) SendSystem(recipientID uint32, subject, body string) error {
|
||||
return svc.mailRepo.SendMail(0, recipientID, subject, body, 0, 0, false, true)
|
||||
}
|
||||
|
||||
// SendGuildInvite sends a guild invitation mail (flagged as guild invite).
|
||||
func (svc *MailService) SendGuildInvite(senderID, recipientID uint32, subject, body string) error {
|
||||
return svc.mailRepo.SendMail(senderID, recipientID, subject, body, 0, 0, true, false)
|
||||
}
|
||||
|
||||
// BroadcastToGuild sends a mail from senderID to all members of the specified guild.
|
||||
func (svc *MailService) BroadcastToGuild(senderID, guildID uint32, subject, body string) error {
|
||||
members, err := svc.guildRepo.GetMembers(guildID, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get guild members for broadcast: %w", err)
|
||||
}
|
||||
for _, m := range members {
|
||||
if err := svc.mailRepo.SendMail(senderID, m.CharID, subject, body, 0, 0, false, false); err != nil {
|
||||
return fmt.Errorf("send guild broadcast to char %d: %w", m.CharID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
162
server/channelserver/svc_mail_test.go
Normal file
162
server/channelserver/svc_mail_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestMailService_Send(t *testing.T) {
|
||||
mock := &mockMailRepo{}
|
||||
logger, _ := zap.NewDevelopment()
|
||||
svc := NewMailService(mock, nil, logger)
|
||||
|
||||
err := svc.Send(1, 42, "Hello", "World", 500, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if len(mock.sentMails) != 1 {
|
||||
t.Fatalf("Expected 1 mail, got %d", len(mock.sentMails))
|
||||
}
|
||||
m := mock.sentMails[0]
|
||||
if m.senderID != 1 {
|
||||
t.Errorf("SenderID = %d, want 1", m.senderID)
|
||||
}
|
||||
if m.recipientID != 42 {
|
||||
t.Errorf("RecipientID = %d, want 42", m.recipientID)
|
||||
}
|
||||
if m.subject != "Hello" {
|
||||
t.Errorf("Subject = %q, want %q", m.subject, "Hello")
|
||||
}
|
||||
if m.itemID != 500 {
|
||||
t.Errorf("ItemID = %d, want 500", m.itemID)
|
||||
}
|
||||
if m.itemAmount != 3 {
|
||||
t.Errorf("Quantity = %d, want 3", m.itemAmount)
|
||||
}
|
||||
if m.isGuildInvite || m.isSystemMessage {
|
||||
t.Error("Should not be guild invite or system message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailService_Send_Error(t *testing.T) {
|
||||
mock := &mockMailRepo{sendErr: errors.New("db fail")}
|
||||
logger, _ := zap.NewDevelopment()
|
||||
svc := NewMailService(mock, nil, logger)
|
||||
|
||||
err := svc.Send(1, 42, "Hello", "World", 0, 0)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailService_SendSystem(t *testing.T) {
|
||||
mock := &mockMailRepo{}
|
||||
logger, _ := zap.NewDevelopment()
|
||||
svc := NewMailService(mock, nil, logger)
|
||||
|
||||
err := svc.SendSystem(42, "System Alert", "Something happened")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if len(mock.sentMails) != 1 {
|
||||
t.Fatalf("Expected 1 mail, got %d", len(mock.sentMails))
|
||||
}
|
||||
m := mock.sentMails[0]
|
||||
if m.senderID != 0 {
|
||||
t.Errorf("SenderID = %d, want 0 (system)", m.senderID)
|
||||
}
|
||||
if m.recipientID != 42 {
|
||||
t.Errorf("RecipientID = %d, want 42", m.recipientID)
|
||||
}
|
||||
if !m.isSystemMessage {
|
||||
t.Error("Should be system message")
|
||||
}
|
||||
if m.isGuildInvite {
|
||||
t.Error("Should not be guild invite")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailService_SendGuildInvite(t *testing.T) {
|
||||
mock := &mockMailRepo{}
|
||||
logger, _ := zap.NewDevelopment()
|
||||
svc := NewMailService(mock, nil, logger)
|
||||
|
||||
err := svc.SendGuildInvite(1, 42, "Invite", "Join us")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if len(mock.sentMails) != 1 {
|
||||
t.Fatalf("Expected 1 mail, got %d", len(mock.sentMails))
|
||||
}
|
||||
m := mock.sentMails[0]
|
||||
if !m.isGuildInvite {
|
||||
t.Error("Should be guild invite")
|
||||
}
|
||||
if m.isSystemMessage {
|
||||
t.Error("Should not be system message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailService_BroadcastToGuild(t *testing.T) {
|
||||
mailMock := &mockMailRepo{}
|
||||
guildMock := &mockGuildRepoForMail{
|
||||
members: []*GuildMember{
|
||||
{CharID: 100},
|
||||
{CharID: 200},
|
||||
{CharID: 300},
|
||||
},
|
||||
}
|
||||
logger, _ := zap.NewDevelopment()
|
||||
svc := NewMailService(mailMock, guildMock, logger)
|
||||
|
||||
err := svc.BroadcastToGuild(1, 10, "News", "Update")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if len(mailMock.sentMails) != 3 {
|
||||
t.Fatalf("Expected 3 mails, got %d", len(mailMock.sentMails))
|
||||
}
|
||||
recipients := map[uint32]bool{}
|
||||
for _, m := range mailMock.sentMails {
|
||||
recipients[m.recipientID] = true
|
||||
if m.senderID != 1 {
|
||||
t.Errorf("SenderID = %d, want 1", m.senderID)
|
||||
}
|
||||
}
|
||||
if !recipients[100] || !recipients[200] || !recipients[300] {
|
||||
t.Errorf("Expected recipients 100, 200, 300, got %v", recipients)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailService_BroadcastToGuild_GetMembersError(t *testing.T) {
|
||||
mailMock := &mockMailRepo{}
|
||||
guildMock := &mockGuildRepoForMail{getMembersErr: errors.New("db fail")}
|
||||
logger, _ := zap.NewDevelopment()
|
||||
svc := NewMailService(mailMock, guildMock, logger)
|
||||
|
||||
err := svc.BroadcastToGuild(1, 10, "News", "Update")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
if len(mailMock.sentMails) != 0 {
|
||||
t.Errorf("No mails should be sent on error, got %d", len(mailMock.sentMails))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailService_BroadcastToGuild_SendError(t *testing.T) {
|
||||
mailMock := &mockMailRepo{sendErr: errors.New("db fail")}
|
||||
guildMock := &mockGuildRepoForMail{
|
||||
members: []*GuildMember{
|
||||
{CharID: 100},
|
||||
},
|
||||
}
|
||||
logger, _ := zap.NewDevelopment()
|
||||
svc := NewMailService(mailMock, guildMock, logger)
|
||||
|
||||
err := svc.BroadcastToGuild(1, 10, "News", "Update")
|
||||
if err == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
}
|
||||
@@ -43,44 +43,45 @@ type Config struct {
|
||||
// own locks internally and may be acquired at any point.
|
||||
type Server struct {
|
||||
sync.Mutex
|
||||
Registry ChannelRegistry
|
||||
ID uint16
|
||||
GlobalID string
|
||||
IP string
|
||||
Port uint16
|
||||
logger *zap.Logger
|
||||
db *sqlx.DB
|
||||
charRepo CharacterRepo
|
||||
guildRepo GuildRepo
|
||||
userRepo UserRepo
|
||||
gachaRepo GachaRepo
|
||||
houseRepo HouseRepo
|
||||
festaRepo FestaRepo
|
||||
towerRepo TowerRepo
|
||||
rengokuRepo RengokuRepo
|
||||
mailRepo MailRepo
|
||||
stampRepo StampRepo
|
||||
distRepo DistributionRepo
|
||||
sessionRepo SessionRepo
|
||||
eventRepo EventRepo
|
||||
achievementRepo AchievementRepo
|
||||
shopRepo ShopRepo
|
||||
cafeRepo CafeRepo
|
||||
goocooRepo GoocooRepo
|
||||
divaRepo DivaRepo
|
||||
miscRepo MiscRepo
|
||||
scenarioRepo ScenarioRepo
|
||||
mercenaryRepo MercenaryRepo
|
||||
Registry ChannelRegistry
|
||||
ID uint16
|
||||
GlobalID string
|
||||
IP string
|
||||
Port uint16
|
||||
logger *zap.Logger
|
||||
db *sqlx.DB
|
||||
charRepo CharacterRepo
|
||||
guildRepo GuildRepo
|
||||
userRepo UserRepo
|
||||
gachaRepo GachaRepo
|
||||
houseRepo HouseRepo
|
||||
festaRepo FestaRepo
|
||||
towerRepo TowerRepo
|
||||
rengokuRepo RengokuRepo
|
||||
mailRepo MailRepo
|
||||
stampRepo StampRepo
|
||||
distRepo DistributionRepo
|
||||
sessionRepo SessionRepo
|
||||
eventRepo EventRepo
|
||||
achievementRepo AchievementRepo
|
||||
shopRepo ShopRepo
|
||||
cafeRepo CafeRepo
|
||||
goocooRepo GoocooRepo
|
||||
divaRepo DivaRepo
|
||||
miscRepo MiscRepo
|
||||
scenarioRepo ScenarioRepo
|
||||
mercenaryRepo MercenaryRepo
|
||||
mailService *MailService
|
||||
guildService *GuildService
|
||||
achievementService *AchievementService
|
||||
gachaService *GachaService
|
||||
erupeConfig *cfg.Config
|
||||
acceptConns chan net.Conn
|
||||
deleteConns chan net.Conn
|
||||
sessions map[net.Conn]*Session
|
||||
listener net.Listener // Listener that is created when Server.Start is called.
|
||||
isShuttingDown bool
|
||||
done chan struct{} // Closed on Shutdown to wake background goroutines.
|
||||
erupeConfig *cfg.Config
|
||||
acceptConns chan net.Conn
|
||||
deleteConns chan net.Conn
|
||||
sessions map[net.Conn]*Session
|
||||
listener net.Listener // Listener that is created when Server.Start is called.
|
||||
isShuttingDown bool
|
||||
done chan struct{} // Closed on Shutdown to wake background goroutines.
|
||||
|
||||
stages StageMap
|
||||
|
||||
@@ -156,7 +157,8 @@ func NewServer(config *Config) *Server {
|
||||
s.scenarioRepo = NewScenarioRepository(config.DB)
|
||||
s.mercenaryRepo = NewMercenaryRepository(config.DB)
|
||||
|
||||
s.guildService = NewGuildService(s.guildRepo, s.mailRepo, s.charRepo, s.logger)
|
||||
s.mailService = NewMailService(s.mailRepo, s.guildRepo, s.logger)
|
||||
s.guildService = NewGuildService(s.guildRepo, s.mailService, s.charRepo, s.logger)
|
||||
s.achievementService = NewAchievementService(s.achievementRepo, s.logger)
|
||||
s.gachaService = NewGachaService(s.gachaRepo, s.userRepo, s.charRepo, s.logger, config.ErupeConfig.GameplayOptions.MaximumNP)
|
||||
|
||||
|
||||
@@ -55,10 +55,17 @@ func createMockServer() *Server {
|
||||
return s
|
||||
}
|
||||
|
||||
// ensureMailService wires the MailService from the server's current repos.
|
||||
// Call this after setting mailRepo and guildRepo on the mock server.
|
||||
func ensureMailService(s *Server) {
|
||||
s.mailService = NewMailService(s.mailRepo, s.guildRepo, s.logger)
|
||||
}
|
||||
|
||||
// ensureGuildService wires the GuildService from the server's current repos.
|
||||
// Call this after setting guildRepo, mailRepo, and charRepo on the mock server.
|
||||
func ensureGuildService(s *Server) {
|
||||
s.guildService = NewGuildService(s.guildRepo, s.mailRepo, s.charRepo, s.logger)
|
||||
ensureMailService(s)
|
||||
s.guildService = NewGuildService(s.guildRepo, s.mailService, s.charRepo, s.logger)
|
||||
}
|
||||
|
||||
// ensureAchievementService wires the AchievementService from the server's current repos.
|
||||
|
||||
Reference in New Issue
Block a user