package service import ( "context" "fmt" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/room-service/internal/room/command" ) type giftTargetBilling struct { TargetUserID int64 CommandID string Billing *walletv1.DebitGiftResponse } func (s *Service) debitGiftTargets(ctx context.Context, cmd command.SendGift, roomMeta RoomMeta) (*walletv1.DebitGiftResponse, []giftTargetBilling, error) { if cmd.RobotWalletGift { if len(cmd.TargetUserIDs) != 1 { return nil, nil, xerr.New(xerr.InvalidArgument, "robot gift only supports one target") } billing, err := s.wallet.DebitRobotGift(ctx, &walletv1.DebitRobotGiftRequest{ CommandId: cmd.ID(), RoomId: cmd.RoomID(), SenderUserId: cmd.ActorUserID(), TargetUserId: cmd.TargetUserID, GiftId: cmd.GiftID, GiftCount: cmd.GiftCount, AppCode: appcode.FromContext(ctx), RegionId: roomMeta.VisibleRegionID, SenderRegionId: cmd.SenderRegionID, }) if err != nil { return nil, nil, err } return billing, []giftTargetBilling{{TargetUserID: cmd.TargetUserID, CommandID: cmd.ID(), Billing: billing}}, nil } if len(cmd.TargetUserIDs) == 1 { targetScope := giftTargetHostScopeFor(cmd, cmd.TargetUserID) // 单目标保留原 wallet command_id,避免改变已有幂等键、账务回执和排障路径。 billing, err := s.wallet.DebitGift(ctx, &walletv1.DebitGiftRequest{ CommandId: cmd.ID(), RoomId: cmd.RoomID(), SenderUserId: cmd.ActorUserID(), TargetUserId: cmd.TargetUserID, GiftId: cmd.GiftID, GiftCount: cmd.GiftCount, AppCode: appcode.FromContext(ctx), RegionId: roomMeta.VisibleRegionID, SenderRegionId: cmd.SenderRegionID, TargetIsHost: targetScope.TargetIsHost, TargetHostRegionId: targetScope.TargetHostRegionID, TargetAgencyOwnerUserId: targetScope.TargetAgencyOwnerUserID, EntitlementId: cmd.EntitlementID, ChargeSource: cmd.Source, }) if err != nil { return nil, nil, err } return billing, []giftTargetBilling{{TargetUserID: cmd.TargetUserID, CommandID: cmd.ID(), Billing: billing}}, nil } targets := make([]*walletv1.DebitGiftTarget, 0, len(cmd.TargetUserIDs)) for _, targetUserID := range cmd.TargetUserIDs { targetScope := giftTargetHostScopeFor(cmd, targetUserID) target := &walletv1.DebitGiftTarget{ CommandId: giftTargetCommandID(cmd.ID(), targetUserID, len(cmd.TargetUserIDs)), TargetUserId: targetUserID, } if targetScope.TargetIsHost { // 每个目标只使用 gateway 为该 target 固化的主播快照,避免批量送礼时第一个接收方以外的主播丢工资钻石。 target.TargetIsHost = true target.TargetHostRegionId = targetScope.TargetHostRegionID target.TargetAgencyOwnerUserId = targetScope.TargetAgencyOwnerUserID } targets = append(targets, target) } resp, err := s.wallet.BatchDebitGift(ctx, &walletv1.BatchDebitGiftRequest{ CommandId: cmd.ID(), RoomId: cmd.RoomID(), SenderUserId: cmd.ActorUserID(), GiftId: cmd.GiftID, GiftCount: cmd.GiftCount, AppCode: appcode.FromContext(ctx), RegionId: roomMeta.VisibleRegionID, SenderRegionId: cmd.SenderRegionID, Targets: targets, EntitlementId: cmd.EntitlementID, ChargeSource: cmd.Source, }) if err != nil { return nil, nil, err } targetBillings, err := giftTargetBillingsFromBatch(resp, cmd.TargetUserIDs) if err != nil { return nil, nil, err } return resp.GetAggregate(), targetBillings, nil } func giftTargetHostScopeFor(cmd command.SendGift, targetUserID int64) command.GiftTargetHostScope { for _, scope := range cmd.TargetHostScopes { if scope.TargetUserID == targetUserID { return scope } } if targetUserID == cmd.TargetUserID && cmd.TargetIsHost { // 老 gateway 只会传单目标字段;保留这个兜底,避免滚动发布期间新 room-service 丢旧请求的主播入账。 return command.GiftTargetHostScope{ TargetUserID: targetUserID, TargetIsHost: true, TargetHostRegionID: cmd.TargetHostRegionID, TargetAgencyOwnerUserID: cmd.TargetAgencyOwnerUserID, } } return command.GiftTargetHostScope{TargetUserID: targetUserID} } func giftTargetCommandID(commandID string, targetUserID int64, targetCount int) string { if targetCount <= 1 { return commandID } // 钱包以 command_id 做幂等主键;多目标必须按接收方派生稳定子命令,才能在同一批内保存独立交易。 return fmt.Sprintf("%s:target:%d", commandID, targetUserID) } func giftTargetBillingsFromBatch(resp *walletv1.BatchDebitGiftResponse, targetUserIDs []int64) ([]giftTargetBilling, error) { if resp == nil || resp.GetAggregate() == nil { return nil, xerr.New(xerr.Unavailable, "wallet batch debit response is empty") } receipts := make(map[int64]*walletv1.BatchDebitGiftReceipt, len(resp.GetReceipts())) for _, receipt := range resp.GetReceipts() { if receipt.GetTargetUserId() <= 0 || receipt.GetBilling() == nil { return nil, xerr.New(xerr.Unavailable, "wallet batch debit receipt is incomplete") } receipts[receipt.GetTargetUserId()] = receipt } targetBillings := make([]giftTargetBilling, 0, len(targetUserIDs)) for _, targetUserID := range targetUserIDs { receipt := receipts[targetUserID] if receipt == nil { // 批量钱包响应必须覆盖每个房间目标;少一条就不能提交房间表现和 outbox。 return nil, xerr.New(xerr.Unavailable, "wallet batch debit receipt is missing") } targetBillings = append(targetBillings, giftTargetBilling{ TargetUserID: targetUserID, CommandID: receipt.GetCommandId(), Billing: receipt.GetBilling(), }) } return targetBillings, nil }