fix(server): deliver attendance rewards through idempotent mail and notify new messages

This commit is contained in:
2026-10-05 14:18:34 +08:00
parent 9ece9d9afa
commit 8d657bf694
12 changed files with 705 additions and 140 deletions
+21 -13
View File
@@ -24,6 +24,13 @@ import (
type Economy interface {
Apply(string, []gamedata.Reward, []gamedata.Reward) ([]byte, error)
}
// AttendanceMailIssuer delivers attendance attachments inside the parent
// account transaction. Its durable identity prevents duplicate mail on replay.
type AttendanceMailIssuer interface {
IssueAttachmentsOnce(identity, title, body string, rewards []gamedata.Reward, sentAt time.Time) error
}
type attendance struct {
Group, Count uint64
LastDay string
@@ -69,6 +76,7 @@ type Service struct {
unlocked func(uint64, uint64) bool
authorizeCash func(uint64, uint64) bool
attendancePremium func(uint64) bool
attendanceMail AttendanceMailIssuer
associated func(events.Schedule) uint64
}
@@ -135,6 +143,15 @@ func (s *Service) AttachCashAuthorization(f func(uint64, uint64) bool) {
func (s *Service) AttachAttendancePremium(f func(uint64) bool) { s.attendancePremium = f }
func (s *Service) AttachAssociatedMissionGroup(f func(events.Schedule) uint64) { s.associated = f }
func (s *Service) AttachAttendanceMail(issuer AttendanceMailIssuer) { s.attendanceMail = issuer }
func (s *Service) mailAttendance(identity string, rewards []gamedata.Reward) error {
if s.attendanceMail == nil {
return errors.New("eventtasks: attendance mailbox unavailable")
}
return s.attendanceMail.IssueAttachmentsOnce(identity, "Attendance Rewards", "Your attendance rewards are ready. Please claim the attachments in your mailbox.", rewards, s.now())
}
func (s *Service) SetNewbieStep(step uint64) error {
s.mu.Lock()
defer s.mu.Unlock()
@@ -497,12 +514,9 @@ func (s *Service) handle(path string, b []byte, identity string) ([]byte, error)
}
}
if len(granted) > 0 {
bundle, err := s.economy.Apply(identity, nil, granted)
if err != nil {
if err := s.mailAttendance(identity, granted); err != nil {
return nil, err
}
out = wire.AppendBytes(out, 1001, bundle)
out = wire.AppendString(out, 1002, identity)
}
return out, nil
case "/AttendanceInfo":
@@ -563,18 +577,12 @@ func (s *Service) handle(path string, b []byte, identity string) ([]byte, error)
if len(rewards) == 0 {
return nil, errors.New("eventtasks: attendance day unavailable")
}
bundle, e := s.economy.Apply(identity, nil, rewards)
if e != nil {
return nil, e
if err := s.mailAttendance(identity, rewards); err != nil {
return nil, err
}
a.Obtained[ck] = true
a.History[ck] = true
// Frozen EventRewardResponse has no known fields. The owned-client plugin
// applies this actual grant once using the durable request receipt; Handle
// persists the complete response for replay before the account commits.
out := wire.AppendBytes(nil, 1001, bundle)
out = wire.AppendString(out, 1002, identity)
return out, nil
return nil, nil
case "/EventMissionInfo":
var out []byte
for _, v := range s.taskSchedules() {