196 lines
8.8 KiB
Go
196 lines
8.8 KiB
Go
package broadcast
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"google.golang.org/protobuf/proto"
|
||
luckygifteventsv1 "hyapp.local/api/proto/events/luckygift/v1"
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/luckygiftmq"
|
||
"hyapp/pkg/xerr"
|
||
broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
|
||
)
|
||
|
||
// HandleLuckyGiftEvent 把 lucky-gift-service 在钱包返奖成功后发布的 owner fact 投影成区域 IM outbox。
|
||
// MQ 事实只负责描述已发生的中奖;activity-service 独立拥有区域展示策略和腾讯 IM 投递状态,不能反写 owner outbox。
|
||
func (s *Service) HandleLuckyGiftEvent(ctx context.Context, envelope *luckygifteventsv1.EventEnvelope) (broadcastdomain.ConsumeLuckyGiftEventResult, error) {
|
||
if envelope == nil {
|
||
return broadcastdomain.ConsumeLuckyGiftEventResult{}, xerr.New(xerr.InvalidArgument, "lucky gift event envelope is required")
|
||
}
|
||
result := broadcastdomain.ConsumeLuckyGiftEventResult{
|
||
EventID: envelope.GetEventId(),
|
||
Status: broadcastdomain.StatusSkipped,
|
||
}
|
||
if envelope.GetEventType() != luckygiftmq.EventTypeLuckyGiftDrawn {
|
||
// 独立 topic 未来可能增加审计类事实;未知类型必须确认跳过,不能误生成客户端无法识别的播报。
|
||
return result, nil
|
||
}
|
||
|
||
var event luckygifteventsv1.LuckyGiftDrawn
|
||
if err := proto.Unmarshal(envelope.GetBody(), &event); err != nil {
|
||
return broadcastdomain.ConsumeLuckyGiftEventResult{}, fmt.Errorf("decode lucky gift drawn body: %w", err)
|
||
}
|
||
if err := validateLuckyGiftDrawnEnvelope(envelope, &event); err != nil {
|
||
return broadcastdomain.ConsumeLuckyGiftEventResult{}, err
|
||
}
|
||
// 区域顶部飘屏只接受已经实际到账的 10 倍及以上奖励。pending/failed 事实、0 奖励和无区域事实
|
||
// 都属于合法跳过,避免展示先于钱包到账或把区域未知事件扩大成全局消息。
|
||
if !shouldPublishLuckyGiftBigWin(&event) {
|
||
return result, nil
|
||
}
|
||
|
||
eventCtx := appcode.WithContext(ctx, envelope.GetAppCode())
|
||
sender, err := s.luckyGiftSenderProfile(eventCtx, &event)
|
||
if err != nil {
|
||
// 资料查询是完整 IM 快照的一部分;暂时失败必须交还 MQ 重试,不能先确认后永久退化成错误昵称。
|
||
return broadcastdomain.ConsumeLuckyGiftEventResult{}, fmt.Errorf("resolve lucky gift sender profile: %w", err)
|
||
}
|
||
broadcastEventID := "lucky_gift_big_win:" + event.GetDrawId()
|
||
payloadJSON, err := luckyGiftBigWinPayload(envelope, &event, broadcastEventID, sender, s.now().UTC().UnixMilli())
|
||
if err != nil {
|
||
return broadcastdomain.ConsumeLuckyGiftEventResult{}, err
|
||
}
|
||
published, err := s.PublishRegionBroadcast(eventCtx, PublishInput{
|
||
EventID: broadcastEventID,
|
||
BroadcastType: broadcastdomain.TypeLuckyGiftBigWin,
|
||
RegionID: event.GetVisibleRegionId(),
|
||
PayloadJSON: payloadJSON,
|
||
})
|
||
if err != nil {
|
||
return broadcastdomain.ConsumeLuckyGiftEventResult{}, err
|
||
}
|
||
// im_broadcast_outbox 的 (app_code,event_id) 是本投影的幂等事实;MQ 重投只会得到 Created=false,
|
||
// 不会生成第二条区域消息,也不需要读取 lucky-gift-service 的 owner outbox 状态。
|
||
return broadcastdomain.ConsumeLuckyGiftEventResult{
|
||
EventID: envelope.GetEventId(),
|
||
Status: published.Status,
|
||
BroadcastEventID: published.EventID,
|
||
BroadcastCreated: published.Created,
|
||
}, nil
|
||
}
|
||
|
||
func validateLuckyGiftDrawnEnvelope(envelope *luckygifteventsv1.EventEnvelope, event *luckygifteventsv1.LuckyGiftDrawn) error {
|
||
if strings.TrimSpace(envelope.GetAppCode()) == "" || strings.TrimSpace(envelope.GetEventId()) == "" {
|
||
return xerr.New(xerr.InvalidArgument, "lucky gift event identity is incomplete")
|
||
}
|
||
drawID := strings.TrimSpace(event.GetDrawId())
|
||
if drawID == "" || strings.TrimSpace(event.GetRoomId()) == "" || event.GetSenderUserId() <= 0 {
|
||
return xerr.New(xerr.InvalidArgument, "lucky gift drawn fact is incomplete")
|
||
}
|
||
if envelopeDrawID := strings.TrimSpace(envelope.GetDrawId()); envelopeDrawID != "" && envelopeDrawID != drawID {
|
||
return xerr.New(xerr.InvalidArgument, "lucky gift envelope draw_id does not match body")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func shouldPublishLuckyGiftBigWin(event *luckygifteventsv1.LuckyGiftDrawn) bool {
|
||
if event == nil || !strings.EqualFold(strings.TrimSpace(event.GetRewardStatus()), "granted") {
|
||
return false
|
||
}
|
||
return event.GetEffectiveRewardCoins() > 0 &&
|
||
event.GetVisibleRegionId() > 0 &&
|
||
event.GetMultiplierPpm() >= luckygiftmq.LuckyGiftDisplayMinMultiplierPPM
|
||
}
|
||
|
||
func (s *Service) luckyGiftSenderProfile(ctx context.Context, event *luckygifteventsv1.LuckyGiftDrawn) (SenderProfile, error) {
|
||
profile := SenderProfile{
|
||
UserID: event.GetSenderUserId(),
|
||
Account: strings.TrimSpace(event.GetSenderDisplayUserId()),
|
||
PrettyDisplayUserID: strings.TrimSpace(event.GetSenderPrettyDisplayUserId()),
|
||
Nickname: strings.TrimSpace(event.GetSenderName()),
|
||
Avatar: strings.TrimSpace(event.GetSenderAvatar()),
|
||
}
|
||
// 送礼主链路已经把展示快照写进 owner fact;字段齐全时禁止回查当前资料,避免用户改名后历史事实漂移。
|
||
if profile.Nickname != "" && profile.Account != "" {
|
||
return profile, nil
|
||
}
|
||
resolved, err := s.broadcastUserProfile(ctx, profile.UserID)
|
||
if err != nil {
|
||
return SenderProfile{}, err
|
||
}
|
||
if profile.Account == "" {
|
||
profile.Account = strings.TrimSpace(resolved.Account)
|
||
}
|
||
if profile.PrettyDisplayUserID == "" {
|
||
profile.PrettyDisplayUserID = strings.TrimSpace(resolved.PrettyDisplayUserID)
|
||
}
|
||
if profile.Nickname == "" {
|
||
profile.Nickname = firstLuckyGiftDisplayValue(resolved.Nickname, profile.PrettyDisplayUserID, profile.Account)
|
||
}
|
||
if profile.Avatar == "" {
|
||
profile.Avatar = strings.TrimSpace(resolved.Avatar)
|
||
}
|
||
return profile, nil
|
||
}
|
||
|
||
func firstLuckyGiftDisplayValue(values ...string) string {
|
||
for _, value := range values {
|
||
if normalized := strings.TrimSpace(value); normalized != "" {
|
||
return normalized
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func luckyGiftBigWinPayload(envelope *luckygifteventsv1.EventEnvelope, event *luckygifteventsv1.LuckyGiftDrawn, eventID string, sender SenderProfile, sentAtMS int64) (string, error) {
|
||
createdAtMS := event.GetDrawCreatedAtMs()
|
||
if createdAtMS <= 0 {
|
||
createdAtMS = envelope.GetOccurredAtMs()
|
||
}
|
||
// 字段保持拆服前 Flutter 已消费的扁平协议,并补充 granted/pretty ID 快照;不携带 gift_name,
|
||
// 客户端继续用 gift_id 命中本地多语言礼物配置,避免服务端文案覆盖客户端翻译。
|
||
payload := map[string]any{
|
||
"event_type": "lucky_gift_drawn",
|
||
"event_id": eventID,
|
||
"broadcast_type": broadcastdomain.TypeLuckyGiftBigWin,
|
||
"scope": broadcastdomain.ScopeRegion,
|
||
"app_code": envelope.GetAppCode(),
|
||
"region_id": event.GetVisibleRegionId(),
|
||
"visible_region_id": event.GetVisibleRegionId(),
|
||
"draw_id": event.GetDrawId(),
|
||
"command_id": event.GetCommandId(),
|
||
"pool_id": event.GetPoolId(),
|
||
"room_id": event.GetRoomId(),
|
||
"gift_id": event.GetGiftId(),
|
||
"gift_count": event.GetGiftCount(),
|
||
"user_id": event.GetSenderUserId(),
|
||
"sender_user_id": event.GetSenderUserId(),
|
||
"target_user_id": event.GetTargetUserId(),
|
||
"sender_name": sender.Nickname,
|
||
"sender_avatar": sender.Avatar,
|
||
"sender_display_user_id": sender.Account,
|
||
"sender_pretty_display_user_id": sender.PrettyDisplayUserID,
|
||
"coin_spent": event.GetCoinSpent(),
|
||
"rule_version": event.GetRuleVersion(),
|
||
"experience_pool": event.GetExperiencePool(),
|
||
"selected_tier_id": event.GetSelectedTierId(),
|
||
"multiplier_ppm": event.GetMultiplierPpm(),
|
||
"base_reward_coins": event.GetBaseRewardCoins(),
|
||
"effective_reward_coins": event.GetEffectiveRewardCoins(),
|
||
"reward_status": "granted",
|
||
"wallet_transaction_id": event.GetWalletTransactionId(),
|
||
"stage_feedback": event.GetStageFeedback(),
|
||
"high_multiplier": event.GetHighMultiplier(),
|
||
"created_at_ms": createdAtMS,
|
||
"draw_created_at_ms": createdAtMS,
|
||
"reward_granted_at_ms": event.GetRewardGrantedAtMs(),
|
||
"sent_at_ms": sentAtMS,
|
||
"action": map[string]any{
|
||
"type": "enter_room",
|
||
"room_id": event.GetRoomId(),
|
||
},
|
||
"sender": map[string]any{
|
||
"user_id": sender.UserID,
|
||
"display_user_id": sender.Account,
|
||
"pretty_display_user_id": sender.PrettyDisplayUserID,
|
||
"nickname": sender.Nickname,
|
||
"avatar": sender.Avatar,
|
||
},
|
||
}
|
||
encoded, err := json.Marshal(payload)
|
||
return string(encoded), err
|
||
}
|