package service import ( "strings" "time" roomeventsv1 "hyapp.local/api/proto/events/room/v1" "hyapp/services/room-service/internal/room/command" "hyapp/services/room-service/internal/room/outbox" "hyapp/services/room-service/internal/room/state" ) func buildRoomGiftSentRecords(roomID string, roomVersion int64, now time.Time, cmd command.SendGift, roomMeta RoomMeta, targetBillings []giftTargetBilling, targetCurrentGiftValues map[int64]int64) ([]outbox.Record, error) { // RoomGiftSent 是统计、活动、成长任务和房间展示共同消费的送礼事实;构造时必须按目标拆分,不能用 batch 展示消息替代逐目标账务事实。 records := make([]outbox.Record, 0, len(targetBillings)) for _, targetBilling := range targetBillings { targetDisplayProfile := giftDisplayProfileForUser(cmd.TargetDisplayProfiles, targetBilling.TargetUserID) record, err := outbox.Build(roomID, "RoomGiftSent", roomVersion, now, &roomeventsv1.RoomGiftSent{ SenderUserId: cmd.ActorUserID(), TargetUserId: targetBilling.TargetUserID, GiftId: cmd.GiftID, PoolId: cmd.PoolID, GiftCount: cmd.GiftCount, GiftValue: targetBilling.Billing.GetHeatValue(), CoinSpent: targetBilling.Billing.GetCoinSpent(), BillingReceiptId: targetBilling.Billing.GetBillingReceiptId(), VisibleRegionId: roomMeta.VisibleRegionID, CountryId: cmd.SenderCountryID, RegionId: firstNonZeroInt64(cmd.SenderRegionID, roomMeta.VisibleRegionID), CommandId: targetBilling.CommandID, GiftTypeCode: targetBilling.Billing.GetGiftTypeCode(), CpRelationType: targetBilling.Billing.GetCpRelationType(), GiftName: targetBilling.Billing.GetGiftName(), GiftIconUrl: targetBilling.Billing.GetGiftIconUrl(), GiftAnimationUrl: targetBilling.Billing.GetGiftAnimationUrl(), TargetGiftValue: targetCurrentGiftValues[targetBilling.TargetUserID], SenderName: giftDisplayName(cmd.SenderDisplayProfile), SenderAvatar: strings.TrimSpace(cmd.SenderDisplayProfile.Avatar), SenderDisplayUserId: strings.TrimSpace(cmd.SenderDisplayProfile.DisplayUserID), SenderPrettyDisplayUserId: strings.TrimSpace(cmd.SenderDisplayProfile.PrettyDisplayUserID), ReceiverNickname: giftDisplayName(targetDisplayProfile), ReceiverAvatar: strings.TrimSpace(targetDisplayProfile.Avatar), ReceiverDisplayUserId: strings.TrimSpace(targetDisplayProfile.DisplayUserID), ReceiverPrettyDisplayUserId: strings.TrimSpace(targetDisplayProfile.PrettyDisplayUserID), // display_mode 只改变房间展示投递策略;逐目标事实仍然进入 durable outbox,保证统计、activity、CP 和礼物墙消费口径不变。 DisplayMode: strings.TrimSpace(cmd.DisplayMode), // effect_types 来自 wallet-service 已锁定的礼物价格配置;room-service 只透传,避免统计和全服广播各自重新判定礼物效果。 GiftEffectTypes: targetBilling.Billing.GetGiftEffectTypes(), IsRobotGift: cmd.RobotWalletGift, SyntheticLuckyGift: cmd.SyntheticLuckyGift, RealRoomRobotGift: cmd.RobotWalletGift && !cmd.RobotGift, }) if err != nil { return nil, err } records = append(records, record) } return records, nil } func buildRoomHeatChangedRecord(roomID string, roomVersion int64, now time.Time, heatDelta int64, currentHeat int64) (outbox.Record, error) { // 热度事件只服务房间 UI 展示;当前值仍以 Room Cell 快照为准,客户端丢消息后可通过 snapshot 修复。 return outbox.Build(roomID, "RoomHeatChanged", roomVersion, now, &roomeventsv1.RoomHeatChanged{ Delta: heatDelta, CurrentHeat: currentHeat, }) } func buildRoomRankChangedRecord(roomID string, roomVersion int64, now time.Time, rankItem state.RankItem) (outbox.Record, error) { // 榜单事件表达本次送礼后 sender 在本地礼物榜中的新分值;完整榜单仍由快照和榜单读模型兜底。 return outbox.Build(roomID, "RoomRankChanged", roomVersion, now, &roomeventsv1.RoomRankChanged{ UserId: rankItem.UserID, Score: rankItem.Score, GiftValue: rankItem.GiftValue, }) }