2026-07-13 21:28:41 +08:00

114 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}