164 lines
6.3 KiB
Go
164 lines
6.3 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"crypto/sha256"
|
||
"fmt"
|
||
|
||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/giftlimits"
|
||
"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
|
||
}
|
||
// 短主键沿用首版派生格式,保证滚动发布期间已经进入 saga 的请求仍命中原 wallet/lucky 幂等事实。
|
||
legacy := fmt.Sprintf("%s:target:%d", commandID, targetUserID)
|
||
if len(legacy) <= giftlimits.MaxCommandIDBytes {
|
||
return legacy
|
||
}
|
||
// room 主 command_id 本身允许占满 128 bytes,不能再直接拼接 target。固定域分隔哈希同时绑定
|
||
// parent 和 target,长度恒定且重试稳定;wallet 子交易与 lucky draw 继续共用这一把幂等键。
|
||
digest := sha256.Sum256([]byte(fmt.Sprintf("room-gift-target-v2\x00%s\x00%d", commandID, targetUserID)))
|
||
return fmt.Sprintf("gift-target:v2:%x", digest)
|
||
}
|
||
|
||
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
|
||
}
|