114 lines
5.1 KiB
Go
114 lines
5.1 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"google.golang.org/protobuf/proto"
|
||
luckygifteventsv1 "hyapp.local/api/proto/events/luckygift/v1"
|
||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/luckygiftmq"
|
||
"hyapp/services/room-service/internal/room/outbox"
|
||
)
|
||
|
||
const roomLuckyGiftDrawnEventType = "RoomLuckyGiftDrawn"
|
||
|
||
// HandleLuckyGiftOutboxEvent 把 lucky-gift-service 已完成返奖的 owner 事实持久化为房间展示 outbox。
|
||
// 中奖展示不改变房间状态,因此不能伪造 Room Cell 命令、推进 room_version 或写 command log;
|
||
// room_outbox 的稳定 event_id 同时承担本服务消费幂等和腾讯云 IM 失败补偿。
|
||
func (s *Service) HandleLuckyGiftOutboxEvent(ctx context.Context, envelope *luckygifteventsv1.EventEnvelope) error {
|
||
if s == nil || s.repository == nil {
|
||
return errors.New("room repository is not configured")
|
||
}
|
||
if envelope == nil || envelope.GetEventType() != luckygiftmq.EventTypeLuckyGiftDrawn {
|
||
return errors.New("unexpected lucky gift outbox event")
|
||
}
|
||
|
||
var drawn luckygifteventsv1.LuckyGiftDrawn
|
||
if err := proto.Unmarshal(envelope.GetBody(), &drawn); err != nil {
|
||
return fmt.Errorf("decode lucky gift drawn body: %w", err)
|
||
}
|
||
if err := validateLuckyGiftDrawnEnvelope(envelope, &drawn); err != nil {
|
||
return err
|
||
}
|
||
|
||
// owner 发布全部正向返奖事实;房间金色飘屏只消费达到统一 10x 门槛的 granted 中奖。
|
||
// 低倍事实没有房间副作用,重复投递仍保持同样的 no-op 结果。
|
||
if !strings.EqualFold(strings.TrimSpace(drawn.GetRewardStatus()), "granted") ||
|
||
drawn.GetEffectiveRewardCoins() <= 0 ||
|
||
drawn.GetMultiplierPpm() < luckygiftmq.LuckyGiftDisplayMinMultiplierPPM {
|
||
return nil
|
||
}
|
||
|
||
body := &roomeventsv1.RoomLuckyGiftDrawn{
|
||
DrawId: drawn.GetDrawId(),
|
||
CommandId: drawn.GetCommandId(),
|
||
PoolId: drawn.GetPoolId(),
|
||
GiftId: drawn.GetGiftId(),
|
||
GiftCount: drawn.GetGiftCount(),
|
||
SenderUserId: drawn.GetSenderUserId(),
|
||
TargetUserId: drawn.GetTargetUserId(),
|
||
SenderName: drawn.GetSenderName(),
|
||
SenderAvatar: drawn.GetSenderAvatar(),
|
||
SenderDisplayUserId: drawn.GetSenderDisplayUserId(),
|
||
SenderPrettyDisplayUserId: drawn.GetSenderPrettyDisplayUserId(),
|
||
VisibleRegionId: drawn.GetVisibleRegionId(),
|
||
CountryId: drawn.GetCountryId(),
|
||
CoinSpent: drawn.GetCoinSpent(),
|
||
RuleVersion: drawn.GetRuleVersion(),
|
||
ExperiencePool: drawn.GetExperiencePool(),
|
||
SelectedTierId: drawn.GetSelectedTierId(),
|
||
MultiplierPpm: drawn.GetMultiplierPpm(),
|
||
BaseRewardCoins: drawn.GetBaseRewardCoins(),
|
||
EffectiveRewardCoins: drawn.GetEffectiveRewardCoins(),
|
||
StageFeedback: drawn.GetStageFeedback(),
|
||
HighMultiplier: drawn.GetHighMultiplier(),
|
||
RewardStatus: "granted",
|
||
WalletTransactionId: drawn.GetWalletTransactionId(),
|
||
DrawCreatedAtMs: drawn.GetDrawCreatedAtMs(),
|
||
RewardGrantedAtMs: drawn.GetRewardGrantedAtMs(),
|
||
}
|
||
record, err := outbox.Build(drawn.GetRoomId(), roomLuckyGiftDrawnEventType, 0, time.UnixMilli(envelope.GetOccurredAtMs()).UTC(), body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// owner event_id 是跨服务唯一事实 ID;覆盖 room 本地随机 ID 后,MQ 重投只能命中同一条 room_outbox。
|
||
record.AppCode = strings.TrimSpace(envelope.GetAppCode())
|
||
record.EventID = strings.TrimSpace(envelope.GetEventId())
|
||
record.Envelope.AppCode = record.AppCode
|
||
record.Envelope.EventId = record.EventID
|
||
ctx = appcode.WithContext(ctx, record.AppCode)
|
||
if err := s.repository.SaveOutbox(ctx, []outbox.Record{record}); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 唤醒必须发生在 SaveOutbox 提交之后;重复事实最多产生一个多余唤醒,不会产生第二条 IM outbox。
|
||
s.notifyOutboxCommitted([]outbox.Record{record})
|
||
return nil
|
||
}
|
||
|
||
func validateLuckyGiftDrawnEnvelope(envelope *luckygifteventsv1.EventEnvelope, drawn *luckygifteventsv1.LuckyGiftDrawn) error {
|
||
if envelope == nil || drawn == nil {
|
||
return errors.New("lucky gift drawn fact is required")
|
||
}
|
||
appCode := strings.TrimSpace(envelope.GetAppCode())
|
||
drawID := strings.TrimSpace(drawn.GetDrawId())
|
||
eventID := strings.TrimSpace(envelope.GetEventId())
|
||
if appCode == "" || drawID == "" || drawID != strings.TrimSpace(envelope.GetDrawId()) ||
|
||
strings.TrimSpace(drawn.GetRoomId()) == "" || strings.TrimSpace(drawn.GetCommandId()) == "" ||
|
||
strings.TrimSpace(drawn.GetGiftId()) == "" || drawn.GetGiftCount() <= 0 ||
|
||
drawn.GetSenderUserId() <= 0 || drawn.GetTargetUserId() <= 0 ||
|
||
envelope.GetOccurredAtMs() <= 0 || drawn.GetDrawCreatedAtMs() <= 0 || drawn.GetRewardGrantedAtMs() <= 0 {
|
||
return errors.New("lucky gift drawn fact is incomplete")
|
||
}
|
||
if eventID != "lucky_gift_drawn:"+drawID {
|
||
// 同一 draw 只能映射到一个房间 IM 幂等键;不能接受 producer 临时生成的随机 event_id。
|
||
return errors.New("lucky gift drawn event_id does not match draw_id")
|
||
}
|
||
return nil
|
||
}
|