500 lines
23 KiB
Go
500 lines
23 KiB
Go
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 已结算的 HeatValue,room-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(),
|
||
},
|
||
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.roomUserGiftStats = nil
|
||
result.roomWeeklyContribution = nil
|
||
}
|
||
return result
|
||
}
|