refactor(guild): introduce service layer for guild member operations

Extract business logic from handleMsgMhfOperateGuildMember into
GuildService.OperateMember, establishing the handler→service→repo
layering pattern. The handler is now ~20 lines of protocol glue
(type-assert, map action, call service, send ACK, notify).

GuildService owns authorization checks, repo coordination, mail
composition, and best-effort mail delivery. It accepts plain Go
types (no mhfpacket or Session imports), making it fully testable
with mock repos. Cross-channel notification stays in the handler
since it requires Session.

Adds 7 table-driven service-level tests covering accept/reject/kick,
authorization, repo errors, mail errors, and unknown actions.
This commit is contained in:
Houmgaor
2026-02-23 23:26:46 +01:00
parent 48639942f6
commit 2abca9fb23
6 changed files with 313 additions and 48 deletions

View File

@@ -266,58 +266,32 @@ func handleAvoidLeadershipUpdate(s *Session, pkt *mhfpacket.MsgMhfOperateGuild,
func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfOperateGuildMember)
guild, err := s.server.guildRepo.GetByCharID(pkt.CharID)
if err != nil || guild == nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
actorCharacter, err := s.server.guildRepo.GetCharacterMembership(s.charID)
if err != nil || (!actorCharacter.IsSubLeader() && guild.LeaderCharID != s.charID) {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
var mail Mail
switch pkt.Action {
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_ACCEPT:
err = s.server.guildRepo.AcceptApplication(guild.ID, pkt.CharID)
mail = Mail{
RecipientID: pkt.CharID,
Subject: "Accepted!",
Body: fmt.Sprintf("Your application to join 「%s」 was accepted.", guild.Name),
IsSystemMessage: true,
}
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_REJECT:
err = s.server.guildRepo.RejectApplication(guild.ID, pkt.CharID)
mail = Mail{
RecipientID: pkt.CharID,
Subject: "Rejected",
Body: fmt.Sprintf("Your application to join 「%s」 was rejected.", guild.Name),
IsSystemMessage: true,
}
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_KICK:
err = s.server.guildRepo.RemoveCharacter(pkt.CharID)
mail = Mail{
RecipientID: pkt.CharID,
Subject: "Kicked",
Body: fmt.Sprintf("You were kicked from 「%s」.", guild.Name),
IsSystemMessage: true,
}
default:
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
action, ok := mapMemberAction(pkt.Action)
if !ok {
s.logger.Warn("Unhandled operateGuildMember action", zap.Uint8("action", pkt.Action))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
result, err := s.server.guildService.OperateMember(s.charID, pkt.CharID, action)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
} else {
if err := s.server.mailRepo.SendMail(mail.SenderID, mail.RecipientID, mail.Subject, mail.Body, 0, 0, false, true); err != nil {
s.logger.Warn("Failed to send guild member operation mail", zap.Error(err))
}
s.server.Registry.NotifyMailToCharID(pkt.CharID, s, &mail)
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
s.server.Registry.NotifyMailToCharID(result.MailRecipientID, s, &result.Mail)
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
func mapMemberAction(proto uint8) (GuildMemberAction, bool) {
switch proto {
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_ACCEPT:
return GuildMemberActionAccept, true
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_REJECT:
return GuildMemberActionReject, true
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_KICK:
return GuildMemberActionKick, true
default:
return 0, false
}
}