diff --git a/services/wallet-service/internal/domain/ledger/gift.go b/services/wallet-service/internal/domain/ledger/gift.go index 00327756..1d4cee25 100644 --- a/services/wallet-service/internal/domain/ledger/gift.go +++ b/services/wallet-service/internal/domain/ledger/gift.go @@ -15,11 +15,11 @@ type DebitGiftCommand struct { GiftCount int32 PriceVersion string RegionID int64 - // SenderRegionID 是送礼用户所属区域;用于按区域匹配礼物入主播周期钻石比例。 + // SenderRegionID 是送礼用户所属区域快照;保留给用户归属相关的账务审计和风控语义。 SenderRegionID int64 // TargetIsHost 只能由 gateway 根据 user-service active host profile 注入,客户端输入不可信。 TargetIsHost bool - // TargetHostRegionID 是主播身份所属区域;工资政策按该区域匹配,不能用房间可见区域替代。 + // TargetHostRegionID 是主播身份所属区域;主播周期钻石比例和工资政策均按该区域匹配,不能用房间可见区域替代。 TargetHostRegionID int64 // TargetAgencyOwnerUserID 是送礼瞬间主播上级代理的收款用户快照;结算按快照发放,避免月底组织变化错账。 TargetAgencyOwnerUserID int64 @@ -41,7 +41,7 @@ type DebitGiftTargetCommand struct { TargetUserID int64 // TargetIsHost 只能由 gateway 注入,批量目标之间不能共享该身份快照。 TargetIsHost bool - // TargetHostRegionID 是该目标主播身份所属区域。 + // TargetHostRegionID 是该目标主播身份所属区域;批量结算必须按每个目标自己的区域解析周期钻石比例。 TargetHostRegionID int64 // TargetAgencyOwnerUserID 是该目标主播当前代理 owner 收款快照。 TargetAgencyOwnerUserID int64 diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index dacd6a47..e76850ad 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -577,7 +577,7 @@ func TestDebitGiftAppliesGlobalDefaultGiftDiamondRatioToHeatValueByGiftType(t *t } } -func TestDebitGiftUsesRegionalReturnCoinRatioWithoutChangingHostDiamonds(t *testing.T) { +func TestDebitGiftUsesTargetHostRegionDiamondRatioAndRegionalReturnCoinRatio(t *testing.T) { repository := mysqltest.NewRepository(t) svc := walletservice.New(repository) repository.SetBalance(14501, 500) @@ -602,8 +602,8 @@ func TestDebitGiftUsesRegionalReturnCoinRatioWithoutChangingHostDiamonds(t *test if err != nil { t.Fatalf("DebitGift regional return failed: %v", err) } - if receipt.HeatValue != 100 || receipt.HostPeriodDiamondAdded != 100 { - t.Fatalf("return coin ratio must not change heat or host diamonds: %+v", receipt) + if receipt.HeatValue != 100 || receipt.HostPeriodDiamondAdded != 80 { + t.Fatalf("host region ratio must change host diamonds without changing global room contribution: %+v", receipt) } balances, err := svc.GetBalances(context.Background(), 14502, []string{ledger.AssetCoin}) if err != nil { @@ -698,6 +698,39 @@ func TestBatchDebitGiftAppliesGlobalDefaultGiftDiamondRatioToEachTargetHeatValue } } +func TestBatchDebitGiftUsesEachTargetHostRegionDiamondRatioWithGlobalFallback(t *testing.T) { + repository := mysqltest.NewRepository(t) + repository.SetBalance(15101, 1000) + repository.SetGiftPrice("batch-host-region-ratio-gift", "v1", 100, 100, 100) + repository.SetGiftType("batch-host-region-ratio-gift", resourcedomain.GiftTypeNormal) + repository.SetGiftDiamondRatio(7701, resourcedomain.GiftTypeNormal, "80.00") + svc := walletservice.New(repository) + + receipt, err := svc.BatchDebitGift(context.Background(), ledger.BatchDebitGiftCommand{ + CommandID: "cmd-batch-host-region-ratio", + RoomID: "room-host-region-ratio", + SenderUserID: 15101, + SenderRegionID: 1001, + RegionID: 9901, + GiftID: "batch-host-region-ratio-gift", + GiftCount: 1, + PriceVersion: "v1", + Targets: []ledger.DebitGiftTargetCommand{ + {CommandID: "cmd-batch-host-region-ratio:target:15102", TargetUserID: 15102, TargetIsHost: true, TargetHostRegionID: 7701}, + {CommandID: "cmd-batch-host-region-ratio:target:15103", TargetUserID: 15103, TargetIsHost: true, TargetHostRegionID: 7702}, + }, + }) + if err != nil { + t.Fatalf("BatchDebitGift failed: %v", err) + } + if len(receipt.Targets) != 2 || receipt.Targets[0].Receipt.HostPeriodDiamondAdded != 80 || receipt.Targets[1].Receipt.HostPeriodDiamondAdded != 100 { + t.Fatalf("batch host region ratio or global fallback mismatch: %+v", receipt) + } + if receipt.Targets[0].Receipt.HeatValue != 100 || receipt.Targets[1].Receipt.HeatValue != 100 { + t.Fatalf("host region ratio must not change global room contribution: %+v", receipt) + } +} + // TestDebitGiftRejectsHostPeriodWithoutRegion 锁定工资政策必须有主播区域才能入账。 func TestDebitGiftRejectsHostPeriodWithoutRegion(t *testing.T) { repository := mysqltest.NewRepository(t) diff --git a/services/wallet-service/internal/storage/mysql/gift_batch_debit_repository.go b/services/wallet-service/internal/storage/mysql/gift_batch_debit_repository.go index e0720421..74ecfa26 100644 --- a/services/wallet-service/internal/storage/mysql/gift_batch_debit_repository.go +++ b/services/wallet-service/internal/storage/mysql/gift_batch_debit_repository.go @@ -233,20 +233,28 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb events = append(events, resourceEvent) } - anyHostTarget := false - for _, target := range command.Targets { - if target.TargetIsHost && !hostPointPolicies[target.CommandID].active { - anyHostTarget = true - break - } + type resolvedHostDiamondRatio struct { + ratio giftDiamondRatioSnapshot + effectiveRegionID int64 } - hostRatio := giftDiamondRatioSnapshot{Percent: "0.00", PPM: 0} - hostRatioRegionID := int64(0) - if anyHostTarget { - hostRatio, hostRatioRegionID, err = r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode) + // 批量送礼中的主播可能属于不同区域,必须逐区域解析周期钻石比例;同一区域只查一次, + // 解析器会在区域配置缺失时回退 region_id=0,既避免 N 次重复查询,也不混用其他主播区域。 + hostRatiosByRegion := make(map[int64]resolvedHostDiamondRatio) + for _, target := range command.Targets { + if !target.TargetIsHost || hostPointPolicies[target.CommandID].active { + continue + } + if _, exists := hostRatiosByRegion[target.TargetHostRegionID]; exists { + continue + } + ratio, effectiveRegionID, err := r.resolveGiftDiamondRatio(ctx, tx, command.AppCode, target.TargetHostRegionID, giftConfig.GiftTypeCode) if err != nil { return ledger.BatchGiftReceipt{}, err } + hostRatiosByRegion[target.TargetHostRegionID] = resolvedHostDiamondRatio{ + ratio: ratio, + effectiveRegionID: effectiveRegionID, + } } targetReceipts := make([]ledger.BatchGiftTargetReceipt, 0, len(command.Targets)) @@ -257,13 +265,16 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb giftDiamondRatioPercent := "0.00" giftDiamondRatioRegionID := int64(0) if target.TargetIsHost && !hostPointPolicy.active { - - hostPeriodDiamondAdded, err = giftDiamondAmount(chargeAmount, hostRatio.PPM) + resolvedRatio, exists := hostRatiosByRegion[target.TargetHostRegionID] + if !exists { + return ledger.BatchGiftReceipt{}, xerr.New(xerr.Internal, "host gift diamond ratio is missing") + } + hostPeriodDiamondAdded, err = giftDiamondAmount(chargeAmount, resolvedRatio.ratio.PPM) if err != nil { return ledger.BatchGiftReceipt{}, err } - giftDiamondRatioPercent = hostRatio.Percent - giftDiamondRatioRegionID = hostRatioRegionID + giftDiamondRatioPercent = resolvedRatio.ratio.Percent + giftDiamondRatioRegionID = resolvedRatio.effectiveRegionID hostPeriodCycleKey = hostPeriodCycleKeyFromMS(nowMs) } diff --git a/services/wallet-service/internal/storage/mysql/gift_debit_repository.go b/services/wallet-service/internal/storage/mysql/gift_debit_repository.go index 41fd2b36..2536c375 100644 --- a/services/wallet-service/internal/storage/mysql/gift_debit_repository.go +++ b/services/wallet-service/internal/storage/mysql/gift_debit_repository.go @@ -130,7 +130,9 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm giftDiamondRatioPercent := "0.00" giftDiamondRatioRegionID := int64(0) if command.TargetIsHost && !command.RobotGift && !hostPointPolicyActive { - ratio, ratioRegionID, err := r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode) + // 主播周期钻石按主播身份所属区域结算,而不是房间可见区域或送礼人区域; + // 区域未配置时由解析器回退 region_id=0,保证历史全局配置继续生效。 + ratio, ratioRegionID, err := r.resolveGiftDiamondRatio(ctx, tx, command.AppCode, command.TargetHostRegionID, giftConfig.GiftTypeCode) if err != nil { return ledger.Receipt{}, err }