2026-06-27 16:59:30 +08:00

155 lines
5.8 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"
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
}