2026-07-06 12:43:40 +08:00

502 lines
23 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"
"fmt"
"strings"
"time"
activityv1 "hyapp.local/api/proto/activity/v1"
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
roomv1 "hyapp.local/api/proto/room/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/tencentim"
"hyapp/pkg/xerr"
"hyapp/services/room-service/internal/room/command"
"hyapp/services/room-service/internal/room/outbox"
"hyapp/services/room-service/internal/room/rank"
"hyapp/services/room-service/internal/room/state"
)
type giftFlow struct {
s *Service
ctx context.Context
req *roomv1.SendGiftRequest
robot robotGiftOptions
cmd command.SendGift
roomMeta RoomMeta
rocketConfig RoomRocketConfig
billing *walletv1.DebitGiftResponse
// targetBillings 保存 wallet-service 按目标结算后的账务事实;后续事件、麦位热度和 batch 展示都必须从这里读取,不能重新按客户端输入推导价格。
targetBillings []giftTargetBilling
walletDebitMS int64
settledCommand command.SendGift
luckyEnabled bool
luckyGift *roomv1.LuckyGiftDrawResult
luckyGifts []*roomv1.LuckyGiftDrawResult
heatValue int64
targetGiftValueDeltas map[int64]int64
targetCurrentGiftValues map[int64]int64
rocketApply rocketGiftApplyResult
commandPayload []byte
giftEvents []outbox.Record
records []outbox.Record
robotDisplayRecords []outbox.Record
rocketProgress *roomRocketProgressPending
batchDisplay *roomv1.SendGiftBatchDisplay
batchDisplayEnabled bool
suppressDirectIMEventTypes map[string]bool
}
func newGiftFlow(s *Service, ctx context.Context, req *roomv1.SendGiftRequest, options giftSendOptions) *giftFlow {
displayMode := ""
if options.BatchDisplay {
displayMode = "batch"
}
// SendGift 的账务不在 room-service 内结算,但房间表现、热度和榜单由 Room Cell 同步更新。
cmd := command.SendGift{
Base: baseFromMeta(req.GetMeta()),
TargetType: normalizeGiftTargetType(req.GetTargetType()),
TargetUserIDs: normalizeGiftTargetUserIDs(req.GetTargetUserId(), req.GetTargetUserIds()),
GiftID: req.GetGiftId(),
PoolID: strings.TrimSpace(req.GetPoolId()),
GiftCount: req.GetGiftCount(),
EntitlementID: strings.TrimSpace(req.GetEntitlementId()),
Source: strings.TrimSpace(req.GetSource()),
DisplayMode: displayMode,
SenderCountryID: req.GetSenderCountryId(),
SenderRegionID: req.GetSenderRegionId(),
TargetIsHost: req.GetTargetIsHost(),
// 工资区域由 gateway 从 user-service host profile 注入;房间 visible_region_id 只用于房间/礼物可见性。
TargetHostRegionID: req.GetTargetHostRegionId(),
TargetAgencyOwnerUserID: req.GetTargetAgencyOwnerUserId(),
TargetHostScopes: giftTargetHostScopesFromProto(req.GetTargetHostScopes()),
SenderDisplayProfile: giftDisplayProfileFromProto(req.GetSenderDisplayProfile()),
TargetDisplayProfiles: giftDisplayProfilesFromProto(req.GetTargetDisplayProfiles()),
RobotGift: options.Robot.Enabled && !options.Robot.RealRoomHeat,
RobotWalletGift: options.Robot.Enabled,
SyntheticLuckyGift: options.Robot.Enabled && strings.TrimSpace(req.GetPoolId()) != "",
SyntheticLuckyRewardCoins: firstPositiveInt64(options.Robot.SyntheticRewardCoins, 0),
SyntheticLuckyMultiplierPPM: firstPositiveInt64(options.Robot.SyntheticMultiplierPPM, 1000000),
}
if cmd.TargetType == "" {
cmd.TargetType = "user"
}
cmd.RequestedTargetUserIDs = append([]int64(nil), cmd.TargetUserIDs...)
if cmd.TargetType == "user" && len(cmd.TargetUserIDs) > 0 {
cmd.TargetUserID = cmd.TargetUserIDs[0]
}
return &giftFlow{s: s, ctx: ctx, req: req, robot: options.Robot, cmd: cmd, settledCommand: cmd, batchDisplayEnabled: options.BatchDisplay}
}
func (f *giftFlow) validateBeforeDebit(current *state.RoomState) error {
if _, exists := current.OnlineUsers[f.cmd.ActorUserID()]; !exists {
// 送礼者必须在房间业务 presence 内;没有进入 Room Cell 的用户不能触发钱包扣费。
return xerr.New(xerr.NotFound, "sender not in room")
}
if f.cmd.TargetType != "user" {
// v1 HTTP 契约已预留 all_mic/all_room/couple账务拆单和房间表现策略补齐前先 fail-close。
return xerr.New(xerr.InvalidArgument, "target_type is unsupported")
}
if len(f.cmd.TargetUserIDs) == 0 || f.cmd.TargetUserID <= 0 {
return xerr.New(xerr.InvalidArgument, "target_user_ids is required")
}
if f.batchDisplayEnabled {
if len(f.cmd.RequestedTargetUserIDs) < 2 {
// batch-send 只承载“全部人/多人”语义;过滤后可以只剩一个真实送达目标,但原始请求不能退化成单人入口。
return xerr.New(xerr.InvalidArgument, "batch target_user_ids requires at least two users")
}
if !f.retainOnlineBatchTargets(current) {
// 全部目标都已离房时不能做成功空操作,也不能扣费。
return xerr.New(xerr.NotFound, "target not in room")
}
} else {
for _, targetUserID := range f.cmd.TargetUserIDs {
if _, exists := current.OnlineUsers[targetUserID]; !exists {
// 普通 SendGift 继续 fail-close避免旧单目标入口把“目标不存在”静默吞掉。
return xerr.New(xerr.NotFound, "target not in room")
}
}
}
if f.robot.Enabled {
if err := requireRobotGiftParticipants(current, f.cmd.ActorUserID(), f.cmd.TargetUserIDs, f.robot.RobotUserIDs, !f.robot.RealRoomHeat); err != nil {
return err
}
}
if f.cmd.GiftID == "" || f.cmd.GiftCount <= 0 {
// 礼物 ID 和数量是钱包查服务端价格的最小输入,客户端不再提交礼物单价。
return xerr.New(xerr.InvalidArgument, "gift_id and gift_count must be valid")
}
if strings.EqualFold(f.cmd.Source, "bag") && strings.TrimSpace(f.cmd.EntitlementID) == "" {
// 背包送礼只改变扣费来源,不改变房间事件;因此必须在扣费前明确指定要扣的权益,不能让 wallet 猜测某个库存。
return xerr.New(xerr.InvalidArgument, "entitlement_id is required for bag gift")
}
return nil
}
func (f *giftFlow) retainOnlineBatchTargets(current *state.RoomState) bool {
if f == nil || current == nil || len(f.cmd.TargetUserIDs) == 0 {
return false
}
kept := make([]int64, 0, len(f.cmd.TargetUserIDs))
for _, targetUserID := range f.cmd.TargetUserIDs {
if _, exists := current.OnlineUsers[targetUserID]; exists {
kept = append(kept, targetUserID)
}
}
if len(kept) == 0 {
return false
}
if len(kept) == len(f.cmd.TargetUserIDs) {
return true
}
originalPrimaryTargetUserID := f.cmd.TargetUserID
primaryHostScope, hasPrimaryHostScope := giftTargetHostScopeForExact(f.cmd.TargetHostScopes, kept[0])
// 后续 wallet、幸运礼物、RoomGiftSent、RoomGiftBatchSent 和 command log 都只能看到实际送达目标。
f.cmd.TargetUserIDs = kept
f.cmd.TargetUserID = kept[0]
f.cmd.TargetHostScopes = filterGiftTargetHostScopes(f.cmd.TargetHostScopes, kept)
f.cmd.TargetDisplayProfiles = filterGiftDisplayProfiles(f.cmd.TargetDisplayProfiles, kept)
if hasPrimaryHostScope {
f.cmd.TargetIsHost = primaryHostScope.TargetIsHost
f.cmd.TargetHostRegionID = primaryHostScope.TargetHostRegionID
f.cmd.TargetAgencyOwnerUserID = primaryHostScope.TargetAgencyOwnerUserID
} else if originalPrimaryTargetUserID != f.cmd.TargetUserID {
// 旧单目标 host 字段只描述原主目标;主目标被跳过时必须清空,避免工资入账串到剩余用户。
f.cmd.TargetIsHost = false
f.cmd.TargetHostRegionID = 0
f.cmd.TargetAgencyOwnerUserID = 0
}
f.settledCommand.TargetUserIDs = append([]int64(nil), f.cmd.TargetUserIDs...)
f.settledCommand.TargetUserID = f.cmd.TargetUserID
f.settledCommand.TargetIsHost = f.cmd.TargetIsHost
f.settledCommand.TargetHostRegionID = f.cmd.TargetHostRegionID
f.settledCommand.TargetAgencyOwnerUserID = f.cmd.TargetAgencyOwnerUserID
f.settledCommand.TargetHostScopes = append([]command.GiftTargetHostScope(nil), f.cmd.TargetHostScopes...)
f.settledCommand.TargetDisplayProfiles = append([]command.GiftDisplayProfile(nil), f.cmd.TargetDisplayProfiles...)
return true
}
func (f *giftFlow) prepareRoomFacts() error {
roomMeta, exists, err := f.s.repository.GetRoomMeta(f.ctx, f.cmd.RoomID())
if err != nil {
return err
}
if !exists {
return xerr.New(xerr.NotFound, "room not found")
}
f.roomMeta = roomMeta
if f.cmd.PoolID != "" && !f.cmd.SyntheticLuckyGift {
// 客户端显式传 pool_id 时必须先检查活动规则;规则未启用就拒绝,避免先扣费再发现不能抽奖。
if f.s.luckyGift == nil {
return xerr.New(xerr.Unavailable, "lucky gift service is not configured")
}
checkResp, err := f.s.luckyGift.CheckLuckyGift(f.ctx, &activityv1.CheckLuckyGiftRequest{
Meta: activityMetaFromRoom(f.ctx, f.req.GetMeta()),
UserId: f.cmd.ActorUserID(),
RoomId: f.cmd.RoomID(),
GiftId: f.cmd.GiftID,
PoolId: f.cmd.PoolID,
})
if err != nil {
return err
}
if checkResp == nil || !checkResp.GetEnabled() {
return xerr.New(xerr.RuleNotActive, "lucky gift rule is not active")
}
// Check 只说明“允许按该 pool 抽奖”,真实价格、区域可用性和扣费仍以 wallet-service 为准。
f.luckyEnabled = true
}
rocketConfig, err := f.s.roomRocketConfig(f.ctx)
if err != nil {
return err
}
f.rocketConfig = rocketConfig
return nil
}
func (f *giftFlow) debitAndSettle(now time.Time) error {
return f.s.withLuckyGiftSendLock(f.ctx, f.luckyEnabled, f.cmd.ActorUserID(), func() error {
// 钱包扣费在房间状态变更前完成;失败时不写 command log、不进入 Room Cell 提交态。
walletStartedAt := time.Now()
var err error
f.billing, f.targetBillings, err = f.s.debitGiftTargets(f.ctx, f.cmd, f.roomMeta)
f.walletDebitMS = elapsedMS(walletStartedAt)
if err != nil {
return err
}
if f.billing == nil {
return xerr.New(xerr.Unavailable, "wallet debit response is empty")
}
f.settledCommand.BillingReceiptID = f.billing.GetBillingReceiptId()
f.settledCommand.CoinSpent = f.billing.GetCoinSpent()
f.settledCommand.HeatValue = f.billing.GetHeatValue()
f.settledCommand.PriceVersion = f.billing.GetPriceVersion()
f.settledCommand.GiftTypeCode = f.billing.GetGiftTypeCode()
f.settledCommand.HostPeriodDiamondAdded = f.billing.GetHostPeriodDiamondAdded()
f.settledCommand.HostPeriodCycleKey = f.billing.GetHostPeriodCycleKey()
if f.cmd.SyntheticLuckyGift {
f.luckyGift = syntheticLuckyGiftResult(f.cmd, f.targetBillings, now)
if f.luckyGift != nil {
f.luckyGifts = []*roomv1.LuckyGiftDrawResult{f.luckyGift}
}
} else if !f.luckyEnabled {
// 未显式传 pool_id 的旧客户端仍可通过钱包礼物类型触发默认幸运礼物逻辑;新客户端应传明确 pool_id。
f.luckyEnabled = f.s.shouldDrawLuckyGift(f.cmd.PoolID, f.billing.GetGiftTypeCode())
}
if !f.cmd.SyntheticLuckyGift && f.luckyEnabled {
f.luckyGifts, err = f.s.executeLuckyGiftDraws(f.ctx, f.req.GetMeta(), f.cmd, f.roomMeta, f.targetBillings, now)
if err != nil {
// 抽奖失败时整笔送礼失败并依赖 wallet 幂等重试;不能落普通礼物后丢失幸运礼物抽奖事实。
return err
}
if len(f.luckyGifts) > 0 {
// lucky_gifts 保留每个收礼目标的独立抽奖事实lucky_gift 返回整次送礼的聚合表现,复用已有批量送礼的倍率累加语义。
f.luckyGift = aggregateLuckyGiftResults(f.cmd.ID(), f.luckyGifts)
}
}
return nil
})
}
func (f *giftFlow) applyRoomState(now time.Time, current *state.RoomState, localRank *rank.LocalRank) error {
f.heatValue = f.billing.GetHeatValue()
// 扣费成功后Room Cell 同步更新热度和本地礼物榜;这些状态才是 IM 和 snapshot 的权威来源。
current.Heat += f.heatValue
current.GiftRank = localRank.ApplyGift(f.cmd.ActorUserID(), f.heatValue, now)
f.targetGiftValueDeltas = make(map[int64]int64, len(f.targetBillings))
f.targetCurrentGiftValues = make(map[int64]int64, len(f.targetBillings))
for _, targetBilling := range f.targetBillings {
if targetBilling.Billing == nil || targetBilling.TargetUserID <= 0 {
continue
}
targetGiftValue := targetBilling.Billing.GetHeatValue()
if userState := current.OnlineUsers[targetBilling.TargetUserID]; userState != nil {
// 收礼用户热度完全使用 wallet-service 已结算的 HeatValueroom-service 不重新判断普通/幸运/超级幸运比例。
if targetGiftValue > 0 {
userState.GiftValue += targetGiftValue
f.targetGiftValueDeltas[targetBilling.TargetUserID] += targetGiftValue
}
f.targetCurrentGiftValues[targetBilling.TargetUserID] = userState.GiftValue
}
}
if len(f.targetGiftValueDeltas) > 0 {
// 命令日志只保存每个目标本次增量;累计值由在线用户态按 replay 顺序重建。
f.settledCommand.TargetGiftValues = f.targetGiftValueDeltas
}
current.Version++
rocketApply, err := f.s.applyRoomRocketGift(now, current, f.rocketConfig, f.cmd, &f.settledCommand, f.billing, f.roomMeta)
if err != nil {
return err
}
f.rocketApply = rocketApply
commandPayload, err := command.Serialize(f.settledCommand)
if err != nil {
return err
}
f.commandPayload = commandPayload
return nil
}
func (f *giftFlow) buildMutationResult(now time.Time, current *state.RoomState) (mutationResult, []outbox.Record, error) {
giftEvents, err := buildRoomGiftSentRecords(current.RoomID, current.Version, now, f.cmd, f.roomMeta, f.targetBillings, f.targetCurrentGiftValues)
if err != nil {
return mutationResult{}, nil, err
}
f.giftEvents = giftEvents
f.records = make([]outbox.Record, 0, len(f.giftEvents)+4)
f.robotDisplayRecords = make([]outbox.Record, 0, len(f.giftEvents)+5)
// RobotWalletGift 只表示这笔礼物由机器人钱包结算;真实房机器人仍会同步写房间贡献、火箭和麦位热度,但展示事件改为提交后直发 IM避免机器人流量抢占真人主 outbox。
robotDisplayGift := f.cmd.RobotWalletGift
if robotDisplayGift {
f.robotDisplayRecords = append(f.robotDisplayRecords, f.giftEvents...)
} else {
f.records = append(f.records, f.giftEvents...)
}
heatEvent, err := buildRoomHeatChangedRecord(current.RoomID, current.Version, now, f.heatValue, current.Heat)
if err != nil {
return mutationResult{}, nil, err
}
rankItem := findRankItem(current.GiftRank, f.cmd.ActorUserID())
rankEvent, err := buildRoomRankChangedRecord(current.RoomID, current.Version, now, rankItem)
if err != nil {
return mutationResult{}, nil, err
}
if robotDisplayGift {
// 房间热度和麦位收礼热度仍要给客户端展示;是否真实计入统计由 RobotGift 语义单独控制,不能再拿投递通道反推业务真实性。
f.robotDisplayRecords = append(f.robotDisplayRecords, heatEvent, rankEvent)
} else {
f.records = append(f.records, heatEvent, rankEvent)
}
if !f.cmd.RobotGift {
// 真实房间礼物才广播火箭进度;真人房机器人送礼使用机器人钱包结算,但仍是真实房间贡献,所以火箭事件跟随机器人展示直发通道投递。
if f.rocketApply.progressEvent != nil {
progressRecord, err := outbox.Build(current.RoomID, "RoomRocketFuelChanged", current.Version, now, f.rocketApply.progressEvent)
if err != nil {
return mutationResult{}, nil, err
}
progressRecord.AppCode = appcode.FromContext(f.ctx)
if progressRecord.Envelope != nil {
progressRecord.Envelope.AppCode = progressRecord.AppCode
}
// 燃料变化是高频展示事件,不逐笔写 outbox命令提交成功后由内存合并器按 1 秒窗口只落最大进度。
f.rocketProgress = &roomRocketProgressPending{
record: progressRecord,
robotLane: robotDisplayGift,
rocketID: f.rocketApply.progressEvent.GetRocketId(),
level: f.rocketApply.progressEvent.GetLevel(),
currentFuel: f.rocketApply.progressEvent.GetCurrentFuel(),
roomVersion: current.Version,
}
}
if f.rocketApply.ignited != nil {
ignitedEvent, err := outbox.Build(current.RoomID, "RoomRocketIgnited", current.Version, now, f.rocketApply.ignited)
if err != nil {
return mutationResult{}, nil, err
}
if robotDisplayGift {
f.robotDisplayRecords = append(f.robotDisplayRecords, ignitedEvent)
} else {
f.records = append(f.records, ignitedEvent)
}
}
}
if f.cmd.SyntheticLuckyGift && f.luckyGift != nil {
drawEvent, err := outbox.Build(current.RoomID, "RoomRobotLuckyGiftDrawn", current.Version, now, &roomeventsv1.RoomRobotLuckyGiftDrawn{
DrawId: f.luckyGift.GetDrawId(),
CommandId: f.cmd.ID(),
SenderUserId: f.cmd.ActorUserID(),
TargetUserId: f.cmd.TargetUserID,
GiftId: f.cmd.GiftID,
GiftCount: f.cmd.GiftCount,
PoolId: f.cmd.PoolID,
MultiplierPpm: f.luckyGift.GetMultiplierPpm(),
BaseRewardCoins: f.luckyGift.GetBaseRewardCoins(),
RoomAtmosphereRewardCoins: f.luckyGift.GetRoomAtmosphereRewardCoins(),
ActivitySubsidyCoins: f.luckyGift.GetActivitySubsidyCoins(),
EffectiveRewardCoins: f.luckyGift.GetEffectiveRewardCoins(),
CreatedAtMs: f.luckyGift.GetCreatedAtMs(),
VisibleRegionId: f.roomMeta.VisibleRegionID,
IsRobot: true,
Synthetic: true,
})
if err != nil {
return mutationResult{}, nil, err
}
if robotDisplayGift {
f.robotDisplayRecords = append(f.robotDisplayRecords, drawEvent)
} else {
f.records = append(f.records, drawEvent)
}
}
if f.batchDisplayEnabled {
// 新 batch-send 只合并房间展示消息:逐目标 RoomGiftSent 已经在 giftEvents 中保留为 durable 业务事实。
f.batchDisplay = buildSendGiftBatchDisplay(f.cmd, f.billing, f.targetBillings, f.targetCurrentGiftValues, f.luckyGifts, current.Heat)
batchEvent := roomGiftBatchSentEventFromDisplay(f.batchDisplay, f.roomMeta.VisibleRegionID)
batchRecord, err := outbox.Build(current.RoomID, "RoomGiftBatchSent", current.Version, now, batchEvent)
if err != nil {
return mutationResult{}, nil, err
}
f.batchDisplay.EventId = batchRecord.EventID
f.records = append(f.records, batchRecord)
f.suppressDirectIMEventTypes = map[string]bool{"RoomGiftSent": true}
}
return f.toMutationResult(now, current), f.records, nil
}
func (f *giftFlow) toMutationResult(now time.Time, current *state.RoomState) mutationResult {
finalCoinBalanceAfter := giftFinalCoinBalanceAfter(f.billing)
firstTargetDisplayProfile := giftDisplayProfileForUser(f.cmd.TargetDisplayProfiles, f.cmd.TargetUserID)
result := mutationResult{
snapshot: current.ToProto(),
billingReceiptID: f.billing.GetBillingReceiptId(),
coinBalanceAfter: finalCoinBalanceAfter,
giftIncomeBalanceAfter: f.billing.GetGiftIncomeBalanceAfter(),
roomHeat: current.Heat,
giftRank: cloneProtoRank(current.GiftRank),
luckyGift: f.luckyGift,
luckyGifts: f.luckyGifts,
batchDisplay: f.batchDisplay,
suppressDirectIMEventTypes: f.suppressDirectIMEventTypes,
commandPayload: f.commandPayload,
walletDebitMS: f.walletDebitMS,
robotDisplayRecords: f.robotDisplayRecords,
roomRocketProgress: f.rocketProgress,
roomGiftLeaderboard: &RoomGiftLeaderboardIncrement{
AppCode: appcode.FromContext(f.ctx),
RoomID: current.RoomID,
CoinSpent: roomGiftLeaderboardValue(f.billing),
OccurredAtMS: now.UnixMilli(),
},
roomUserContributionPeriodStats: roomContributionPeriodStatIncrements(appcode.FromContext(f.ctx), current.RoomID, f.cmd.ActorUserID(), f.heatValue, now.UnixMilli()),
roomUserGiftStats: []RoomUserGiftStatIncrement{
{
AppCode: appcode.FromContext(f.ctx),
RoomID: current.RoomID,
UserID: f.cmd.ActorUserID(),
GiftValue: f.heatValue,
LastGiftAtMS: now.UnixMilli(),
},
},
roomWeeklyContribution: &RoomWeeklyContributionIncrement{
AppCode: appcode.FromContext(f.ctx),
RoomID: current.RoomID,
GiftValue: f.heatValue,
OccurredMS: now.UnixMilli(),
},
syncEvent: &tencentim.RoomEvent{
// 同步广播只选 GiftSent 主事件作为客户端房间系统消息Heat/Rank 通过字段携带。
EventID: f.giftEvents[0].EventID,
RoomID: current.RoomID,
EventType: "room_gift_sent",
ActorUserID: f.cmd.ActorUserID(),
TargetUserID: f.cmd.TargetUserID,
GiftValue: f.heatValue,
RoomHeat: current.Heat,
RoomVersion: current.Version,
Attributes: map[string]string{
"gift_id": f.cmd.GiftID,
"pool_id": f.cmd.PoolID,
"gift_count": fmt.Sprintf("%d", f.cmd.GiftCount),
"billing_receipt_id": f.billing.GetBillingReceiptId(),
"coin_spent": fmt.Sprintf("%d", f.settledCommand.CoinSpent),
"target_count": fmt.Sprintf("%d", len(f.cmd.TargetUserIDs)),
"target_user_ids": giftTargetUserIDsAttribute(f.cmd.TargetUserIDs),
"price_version": f.settledCommand.PriceVersion,
"host_period_diamond_added": fmt.Sprintf("%d", f.settledCommand.HostPeriodDiamondAdded),
"host_period_cycle_key": f.settledCommand.HostPeriodCycleKey,
"gift_type_code": f.billing.GetGiftTypeCode(),
"cp_relation_type": f.billing.GetCpRelationType(),
"gift_name": f.billing.GetGiftName(),
"gift_icon_url": f.billing.GetGiftIconUrl(),
"gift_animation_url": f.billing.GetGiftAnimationUrl(),
"target_gift_value": fmt.Sprintf("%d", f.targetCurrentGiftValues[f.cmd.TargetUserID]),
"is_robot_gift": fmt.Sprintf("%t", f.cmd.RobotWalletGift),
"synthetic_lucky_gift": fmt.Sprintf("%t", f.cmd.SyntheticLuckyGift),
"sender_name": giftDisplayName(f.cmd.SenderDisplayProfile),
"sender_avatar": strings.TrimSpace(f.cmd.SenderDisplayProfile.Avatar),
"sender_display_user_id": strings.TrimSpace(f.cmd.SenderDisplayProfile.DisplayUserID),
"sender_pretty_display_user_id": strings.TrimSpace(f.cmd.SenderDisplayProfile.PrettyDisplayUserID),
"receiver_nickname": giftDisplayName(firstTargetDisplayProfile),
"receiver_avatar": strings.TrimSpace(firstTargetDisplayProfile.Avatar),
"receiver_display_user_id": strings.TrimSpace(firstTargetDisplayProfile.DisplayUserID),
"receiver_pretty_display_user_id": strings.TrimSpace(firstTargetDisplayProfile.PrettyDisplayUserID),
},
},
}
if f.cmd.RobotGift {
// 机器人房间可以增加房间热度和麦位收礼热度,但不进入跨房礼物榜和房间送礼统计。
result.roomGiftLeaderboard = nil
result.roomUserContributionPeriodStats = nil
result.roomUserGiftStats = nil
result.roomWeeklyContribution = nil
}
return result
}