幸运礼物修复

This commit is contained in:
zhx 2026-05-26 06:07:29 +08:00
parent e7b6ed9fb7
commit 636a0085d0
4 changed files with 50 additions and 4 deletions

View File

@ -10,7 +10,9 @@ import (
walletv1 "hyapp.local/api/proto/wallet/v1" walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode" "hyapp/pkg/appcode"
"hyapp/pkg/tencentim" "hyapp/pkg/tencentim"
broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
domain "hyapp/services/activity-service/internal/domain/luckygift" domain "hyapp/services/activity-service/internal/domain/luckygift"
broadcastservice "hyapp/services/activity-service/internal/service/broadcast"
) )
func TestGetConfigReturnsPoolDefault(t *testing.T) { func TestGetConfigReturnsPoolDefault(t *testing.T) {
@ -54,6 +56,7 @@ func TestProcessPendingDrawOutboxCreditsWalletAndPublishesRoomIM(t *testing.T) {
"user_id": 42, "user_id": 42,
"sender_user_id": 42, "sender_user_id": 42,
"target_user_id": 99, "target_user_id": 99,
"visible_region_id": 210,
"room_id": "room-1", "room_id": "room-1",
"gift_id": "rose", "gift_id": "rose",
"gift_count": 1, "gift_count": 1,
@ -73,7 +76,8 @@ func TestProcessPendingDrawOutboxCreditsWalletAndPublishesRoomIM(t *testing.T) {
} }
wallet := &fakeLuckyGiftWallet{} wallet := &fakeLuckyGiftWallet{}
publisher := &fakeLuckyGiftPublisher{} publisher := &fakeLuckyGiftPublisher{}
service := New(repository, WithWallet(wallet), WithRoomPublisher(publisher)) broadcaster := &fakeLuckyGiftRegionBroadcaster{}
service := New(repository, WithWallet(wallet), WithRoomPublisher(publisher), WithRegionBroadcaster(broadcaster))
service.SetClock(func() time.Time { return time.UnixMilli(1760000001000).UTC() }) service.SetClock(func() time.Time { return time.UnixMilli(1760000001000).UTC() })
processed, err := service.ProcessPendingDrawOutbox(context.Background(), WorkerOptions{WorkerID: "worker-1"}) processed, err := service.ProcessPendingDrawOutbox(context.Background(), WorkerOptions{WorkerID: "worker-1"})
@ -89,6 +93,16 @@ func TestProcessPendingDrawOutboxCreditsWalletAndPublishesRoomIM(t *testing.T) {
if publisher.last.GroupID != "room-1" || publisher.last.Desc != "lucky_gift_drawn" || publisher.last.EventID != "lucky_gift_drawn:lucky_draw_test" { if publisher.last.GroupID != "room-1" || publisher.last.Desc != "lucky_gift_drawn" || publisher.last.EventID != "lucky_gift_drawn:lucky_draw_test" {
t.Fatalf("room im message mismatch: %+v", publisher.last) t.Fatalf("room im message mismatch: %+v", publisher.last)
} }
if broadcaster.last.EventID != "lucky_gift_big_win:lucky_draw_test" || broadcaster.last.BroadcastType != broadcastdomain.TypeLuckyGiftBigWin || broadcaster.last.RegionID != 210 {
t.Fatalf("region lucky broadcast mismatch: %+v", broadcaster.last)
}
var broadcastPayload map[string]any
if err := json.Unmarshal([]byte(broadcaster.last.PayloadJSON), &broadcastPayload); err != nil {
t.Fatalf("decode region broadcast payload failed: %v", err)
}
if broadcastPayload["event_type"] != "lucky_gift_drawn" || broadcastPayload["broadcast_type"] != broadcastdomain.TypeLuckyGiftBigWin || broadcastPayload["room_id"] != "room-1" {
t.Fatalf("region broadcast payload mismatch: %+v", broadcastPayload)
}
if repository.grantedDrawID != "lucky_draw_test" || repository.deliveredOutboxID != "lucky_lucky_draw_test" { if repository.grantedDrawID != "lucky_draw_test" || repository.deliveredOutboxID != "lucky_lucky_draw_test" {
t.Fatalf("repository settlement mismatch: granted=%s delivered=%s", repository.grantedDrawID, repository.deliveredOutboxID) t.Fatalf("repository settlement mismatch: granted=%s delivered=%s", repository.grantedDrawID, repository.deliveredOutboxID)
} }
@ -176,3 +190,12 @@ func (p *fakeLuckyGiftPublisher) PublishGroupCustomMessage(_ context.Context, me
p.last = message p.last = message
return nil return nil
} }
type fakeLuckyGiftRegionBroadcaster struct {
last broadcastservice.PublishInput
}
func (b *fakeLuckyGiftRegionBroadcaster) PublishRegionBroadcast(_ context.Context, input broadcastservice.PublishInput) (broadcastdomain.PublishResult, error) {
b.last = input
return broadcastdomain.PublishResult{EventID: input.EventID, Status: broadcastdomain.StatusPending, Created: true}, nil
}

View File

@ -1326,10 +1326,9 @@ func (r *Repository) updateLuckyRiskCounters(ctx context.Context, tx *sql.Tx, ap
func (r *Repository) selectLuckyCandidate(config domain.Config, pool string, stageFeedback bool, globalWindow, giftWindow luckyRTPWindow, platformPool, roomPool, giftPool, atmosphere luckyPool, activityRemaining int64, counters map[string]luckyCounter, minFutureMax int64) (luckyCandidate, map[string]bool, error) { func (r *Repository) selectLuckyCandidate(config domain.Config, pool string, stageFeedback bool, globalWindow, giftWindow luckyRTPWindow, platformPool, roomPool, giftPool, atmosphere luckyPool, activityRemaining int64, counters map[string]luckyCounter, minFutureMax int64) (luckyCandidate, map[string]bool, error) {
minRequired := maxInt64(globalWindow.minRequired(minFutureMax), giftWindow.minRequired(minFutureMax)) minRequired := maxInt64(globalWindow.minRequired(minFutureMax), giftWindow.minRequired(minFutureMax))
rtpMaxAllowed := minInt64(globalWindow.remainingPayout(), giftWindow.remainingPayout())
poolCapacity := luckyWeightedPoolCapacity(platformPool, roomPool, giftPool, config) poolCapacity := luckyWeightedPoolCapacity(platformPool, roomPool, giftPool, config)
riskCapacity := luckyRiskCapacity(config, counters) riskCapacity := luckyRiskCapacity(config, counters)
baseMaxAllowed := minInt64(rtpMaxAllowed, minInt64(poolCapacity, riskCapacity)) baseMaxAllowed := minInt64(poolCapacity, riskCapacity)
limited := map[string]bool{} limited := map[string]bool{}
baseCandidates := make([]luckyCandidate, 0, len(config.Tiers)) baseCandidates := make([]luckyCandidate, 0, len(config.Tiers))

View File

@ -100,6 +100,26 @@ func TestLuckyAutoWeightedBaseCandidatesDoesNotDiluteDeficitAcrossLargeWindow(t
} }
} }
func TestLuckyAutoWeightedBaseCandidatesInterpolatesSingleDrawBelowOneX(t *testing.T) {
candidates := []luckyCandidate{
{TierID: "none", BaseReward: 0},
{TierID: "rebate_1x", BaseReward: 100},
}
window := luckyRTPWindow{ControlDraws: 100_000, TargetPayoutCoins: 95}
weighted := luckyAutoWeightedBaseCandidates(candidates, 0, window, window)
if len(weighted) != 2 {
t.Fatalf("expected 0x and 1x interpolation candidates, got %#v", weighted)
}
if weighted[0].TierID != "none" || weighted[0].Weight != 50_000 {
t.Fatalf("expected 0x to keep 5%% probability, got %#v", weighted[0])
}
if weighted[1].TierID != "rebate_1x" || weighted[1].Weight != 950_000 {
t.Fatalf("expected 1x to carry 95%% probability, got %#v", weighted[1])
}
}
func TestSelectLuckyCandidateDoesNotInjectNoneTier(t *testing.T) { func TestSelectLuckyCandidateDoesNotInjectNoneTier(t *testing.T) {
config := domain.Config{ config := domain.Config{
GiftID: domain.DefaultPoolID, GiftID: domain.DefaultPoolID,

View File

@ -104,7 +104,11 @@ func TestSendGiftReturnsLuckyGiftDrawResult(t *testing.T) {
t.Fatalf("expected one lucky draw, got %d", len(luckyGift.draws)) t.Fatalf("expected one lucky draw, got %d", len(luckyGift.draws))
} }
drawMeta := luckyGift.draws[0].GetLuckyGift() drawMeta := luckyGift.draws[0].GetLuckyGift()
if drawMeta.GetCommandId() != "cmd-lucky" || drawMeta.GetCoinSpent() != 100 || drawMeta.GetDeviceId() != "device-session-1" || drawMeta.GetAnchorId() != "101" { if drawMeta.GetCommandId() != "cmd-lucky" ||
drawMeta.GetCoinSpent() != 100 ||
drawMeta.GetDeviceId() != "device-session-1" ||
drawMeta.GetAnchorId() != "101" ||
drawMeta.GetVisibleRegionId() != 9001 {
t.Fatalf("lucky draw meta mismatch: %+v", drawMeta) t.Fatalf("lucky draw meta mismatch: %+v", drawMeta)
} }
} }