机器人和礼物问题
This commit is contained in:
parent
c8fc1ede01
commit
3b2e059964
@ -18,6 +18,11 @@ const (
|
||||
robotMaxDisplayLevel = 30
|
||||
)
|
||||
|
||||
var excludedRobotAvatarFrameResourceIDs = map[int64]struct{}{
|
||||
165: {},
|
||||
175: {},
|
||||
}
|
||||
|
||||
type robotAppearanceCatalog struct {
|
||||
avatarFrames []*walletv1.Resource
|
||||
vehicles []*walletv1.Resource
|
||||
@ -102,13 +107,27 @@ func (h *Handler) listRobotResources(ctx context.Context, requestID string, appC
|
||||
}
|
||||
resources := make([]*walletv1.Resource, 0, len(resp.GetResources()))
|
||||
for _, item := range resp.GetResources() {
|
||||
if item != nil && item.GetResourceId() > 0 && item.GetStatus() == "active" {
|
||||
resources = append(resources, item)
|
||||
if item == nil || item.GetResourceId() <= 0 || item.GetStatus() != "active" {
|
||||
continue
|
||||
}
|
||||
if isExcludedRobotAppearanceResource(resourceType, item.GetResourceId()) {
|
||||
// 机器人装扮随机池只过滤明确不适合机器人批量露出的资源;资源仍保留在 wallet owner 中,
|
||||
// 真人发放、商城和其他后台场景不受这个创建机器人专用策略影响。
|
||||
continue
|
||||
}
|
||||
resources = append(resources, item)
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func isExcludedRobotAppearanceResource(resourceType string, resourceID int64) bool {
|
||||
if resourceType != "avatar_frame" {
|
||||
return false
|
||||
}
|
||||
_, excluded := excludedRobotAvatarFrameResourceIDs[resourceID]
|
||||
return excluded
|
||||
}
|
||||
|
||||
func (h *Handler) grantAndEquipRobotResource(ctx context.Context, requestID string, appCode string, actorUserID int64, userID int64, slot string, resourceID int64, equip bool) error {
|
||||
grant, err := h.wallet.GrantResource(ctx, &walletv1.GrantResourceRequest{
|
||||
CommandId: fmt.Sprintf("game_robot_init:%s:%d:%s:%d", requestID, userID, slot, resourceID),
|
||||
|
||||
@ -66,7 +66,7 @@ func (c *fakeRobotGameClient) RegisterDiceRobots(_ context.Context, req *gamev1.
|
||||
type fakeRobotWalletClient struct {
|
||||
walletclient.Client
|
||||
listRequests []*walletv1.ListResourcesRequest
|
||||
grants []string
|
||||
grants []int64
|
||||
equips []int64
|
||||
}
|
||||
|
||||
@ -74,7 +74,11 @@ func (c *fakeRobotWalletClient) ListResources(_ context.Context, req *walletv1.L
|
||||
c.listRequests = append(c.listRequests, req)
|
||||
switch req.GetResourceType() {
|
||||
case "avatar_frame":
|
||||
return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{{ResourceId: 101, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false}}}, nil
|
||||
return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{
|
||||
{ResourceId: 165, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false},
|
||||
{ResourceId: 175, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false},
|
||||
{ResourceId: 101, ResourceType: "avatar_frame", Status: "active", ManagerGrantEnabled: false},
|
||||
}}, nil
|
||||
case "vehicle":
|
||||
return &walletv1.ListResourcesResponse{Resources: []*walletv1.Resource{{ResourceId: 201, ResourceType: "vehicle", Status: "active", ManagerGrantEnabled: false}}}, nil
|
||||
case "badge":
|
||||
@ -88,7 +92,7 @@ func (c *fakeRobotWalletClient) ListResources(_ context.Context, req *walletv1.L
|
||||
}
|
||||
|
||||
func (c *fakeRobotWalletClient) GrantResource(_ context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error) {
|
||||
c.grants = append(c.grants, req.GetCommandId())
|
||||
c.grants = append(c.grants, req.GetResourceId())
|
||||
return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{Items: []*walletv1.ResourceGrantItem{{
|
||||
ResourceId: req.GetResourceId(),
|
||||
EntitlementId: "entitlement",
|
||||
@ -173,6 +177,12 @@ func TestGenerateDiceRobotsUsesLikeiProfiles(t *testing.T) {
|
||||
t.Fatalf("robot level out of range: %+v", level)
|
||||
}
|
||||
}
|
||||
for _, resourceID := range append(wallets.grants, wallets.equips...) {
|
||||
// 165 和 175 是资源库里的有效头像框,但创建机器人不能随机抽中,避免批量机器人展示这两款运营保留资源。
|
||||
if resourceID == 165 || resourceID == 175 {
|
||||
t.Fatalf("robot appearance must exclude avatar frame %d from random grants/equips", resourceID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateDiceRobotsFallsBackToRandomProfilesWhenLikeiMissing(t *testing.T) {
|
||||
|
||||
@ -22,7 +22,7 @@ import (
|
||||
const defaultWheelID = "default"
|
||||
|
||||
var wheelTierCounts = map[string]int{
|
||||
"classic": 9,
|
||||
"classic": 8,
|
||||
"luxury": 8,
|
||||
"advanced": 12,
|
||||
}
|
||||
@ -210,7 +210,7 @@ func validateConfigRequest(req configRequest) error {
|
||||
return fmt.Errorf("wheel_id 不能为空")
|
||||
}
|
||||
expectedCount := expectedWheelTierCount(req.WheelID)
|
||||
// 转盘前端布局是固定格子数:classic=9、luxury=8、advanced=12。这里按池子强校验,
|
||||
// 转盘前端布局是固定格子数:classic=8、luxury=8、advanced=12。这里按池子强校验,
|
||||
// 避免运营导入资源组后少格或多格,导致 H5 动画序列和后端奖品配置无法一一对应。
|
||||
if len(req.Tiers) != expectedCount {
|
||||
return fmt.Errorf("%s 转盘必须配置 %d 个奖品档位", normalizeWheelID(req.WheelID), expectedCount)
|
||||
|
||||
@ -9,9 +9,9 @@ import (
|
||||
|
||||
func TestConfigToProtoSupportsPoolTierCountAndForcesPropRTPZero(t *testing.T) {
|
||||
tiers := validClassicTiers()
|
||||
tiers[0] = tierDTO{DisplayName: "金币", RewardType: " coin ", RewardCount: 1, RewardCoins: 1000, ProbabilityPercent: defaultProbabilityPercent(0, 9), Enabled: true}
|
||||
tiers[1] = tierDTO{DisplayName: "礼物", RewardType: "gift", RewardID: "gift_1", RewardCount: 1, RTPValueCoins: 5000, ProbabilityPercent: defaultProbabilityPercent(1, 9), Enabled: true}
|
||||
tiers[2] = tierDTO{DisplayName: "装扮", RewardType: "dress_up", RewardID: "dress_1", RewardCount: 1, RTPValueCoins: 999999, ProbabilityPercent: defaultProbabilityPercent(2, 9), Enabled: true}
|
||||
tiers[0] = tierDTO{DisplayName: "金币", RewardType: " coin ", RewardCount: 1, RewardCoins: 1000, ProbabilityPercent: defaultProbabilityPercent(0, 8), Enabled: true}
|
||||
tiers[1] = tierDTO{DisplayName: "礼物", RewardType: "gift", RewardID: "gift_1", RewardCount: 1, RTPValueCoins: 5000, ProbabilityPercent: defaultProbabilityPercent(1, 8), Enabled: true}
|
||||
tiers[2] = tierDTO{DisplayName: "装扮", RewardType: "dress_up", RewardID: "dress_1", RewardCount: 1, RTPValueCoins: 999999, ProbabilityPercent: defaultProbabilityPercent(2, 8), Enabled: true}
|
||||
req := configRequest{
|
||||
WheelID: " classic ",
|
||||
Enabled: true,
|
||||
@ -32,8 +32,8 @@ func TestConfigToProtoSupportsPoolTierCountAndForcesPropRTPZero(t *testing.T) {
|
||||
t.Fatalf("ppm mismatch: target=%d pool=%d", config.GetTargetRtpPpm(), config.GetPoolRatePpm())
|
||||
}
|
||||
protoTiers := config.GetTiers()
|
||||
if len(protoTiers) != 9 {
|
||||
t.Fatalf("tier count = %d, want 9", len(protoTiers))
|
||||
if len(protoTiers) != 8 {
|
||||
t.Fatalf("tier count = %d, want 8", len(protoTiers))
|
||||
}
|
||||
if protoTiers[0].GetTierId() != "coin-01" || protoTiers[0].GetRtpValueCoins() != 1000 {
|
||||
t.Fatalf("coin tier mismatch: %+v", protoTiers[0])
|
||||
@ -52,11 +52,11 @@ func TestValidateConfigRequiresPoolTierCountAndProbabilitySum(t *testing.T) {
|
||||
{RewardType: "gift", RewardID: "gift_2", ProbabilityPercent: 20},
|
||||
},
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "9 个奖品档位") {
|
||||
if err == nil || !strings.Contains(err.Error(), "8 个奖品档位") {
|
||||
t.Fatalf("classic tier count should be rejected, got %v", err)
|
||||
}
|
||||
tiers := validClassicTiers()
|
||||
tiers[8].ProbabilityPercent = 1
|
||||
tiers[7].ProbabilityPercent = 1
|
||||
err = validateConfigRequest(configRequest{
|
||||
WheelID: "classic",
|
||||
Tiers: tiers,
|
||||
@ -99,14 +99,14 @@ func TestConfigFromProtoConvertsPPMToPercent(t *testing.T) {
|
||||
}
|
||||
|
||||
func validClassicTiers() []tierDTO {
|
||||
tiers := make([]tierDTO, 0, 9)
|
||||
for index := 0; index < 9; index++ {
|
||||
tiers := make([]tierDTO, 0, 8)
|
||||
for index := 0; index < 8; index++ {
|
||||
tiers = append(tiers, tierDTO{
|
||||
DisplayName: "金币",
|
||||
RewardType: "coin",
|
||||
RewardCount: 1,
|
||||
RewardCoins: 100,
|
||||
ProbabilityPercent: defaultProbabilityPercent(index, 9),
|
||||
ProbabilityPercent: defaultProbabilityPercent(index, 8),
|
||||
Enabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
@ -11,6 +11,12 @@ import (
|
||||
|
||||
const defaultWheelID = "default"
|
||||
|
||||
var wheelTierCounts = map[string]int{
|
||||
"classic": 8,
|
||||
"luxury": 8,
|
||||
"advanced": 12,
|
||||
}
|
||||
|
||||
func NormalizeWheelID(wheelID string) string {
|
||||
wheelID = strings.TrimSpace(wheelID)
|
||||
if wheelID == "" {
|
||||
@ -65,6 +71,11 @@ func ValidateRuleConfig(config domain.RuleConfig) error {
|
||||
if len(config.Tiers) == 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "wheel tiers are required")
|
||||
}
|
||||
if expectedCount, ok := wheelTierCounts[config.WheelID]; ok && len(config.Tiers) != expectedCount {
|
||||
// H5 每个转盘的视觉格子数是固定契约;服务端发布前强校验数量,避免后台或脚本写入多余档位后,
|
||||
// 抽奖结果命中一个 H5 没有位置展示的奖品,造成动画、中奖弹窗和实际发奖对不上。
|
||||
return xerr.New(xerr.InvalidArgument, fmt.Sprintf("wheel %s requires %d tiers", config.WheelID, expectedCount))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
var enabled bool
|
||||
for _, tier := range config.Tiers {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
package wheel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
domain "hyapp/services/activity-service/internal/domain/wheel"
|
||||
@ -27,7 +29,7 @@ func TestNormalizeRuleConfigForcesPropRTPValueToZero(t *testing.T) {
|
||||
|
||||
func TestValidateRuleConfigRejectsPropRTPValueAfterNormalizationByCallerMutation(t *testing.T) {
|
||||
config := NormalizeRuleConfig(domain.RuleConfig{
|
||||
WheelID: "classic",
|
||||
WheelID: "custom",
|
||||
DrawPriceCoins: 100,
|
||||
SettlementWindowDraws: 1000,
|
||||
Tiers: []domain.Tier{
|
||||
@ -39,3 +41,25 @@ func TestValidateRuleConfigRejectsPropRTPValueAfterNormalizationByCallerMutation
|
||||
t.Fatalf("ValidateRuleConfig should normalize prop rtp value before validation: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRuleConfigRejectsClassicNineTiers(t *testing.T) {
|
||||
tiers := make([]domain.Tier, 0, 9)
|
||||
for index := 0; index < 9; index++ {
|
||||
tiers = append(tiers, domain.Tier{
|
||||
TierID: fmt.Sprintf("classic-%d", index+1),
|
||||
RewardType: "coin",
|
||||
RewardCoins: 100,
|
||||
WeightPPM: 1,
|
||||
Enabled: true,
|
||||
})
|
||||
}
|
||||
err := ValidateRuleConfig(domain.RuleConfig{
|
||||
WheelID: "classic",
|
||||
DrawPriceCoins: 100,
|
||||
SettlementWindowDraws: 1000,
|
||||
Tiers: tiers,
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "requires 8 tiers") {
|
||||
t.Fatalf("classic should reject nine tiers, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user