diff --git a/services/activity-service/internal/service/luckygift/config.go b/services/activity-service/internal/service/luckygift/config.go index 0955616a..f571b7a8 100644 --- a/services/activity-service/internal/service/luckygift/config.go +++ b/services/activity-service/internal/service/luckygift/config.go @@ -162,6 +162,7 @@ func validateRuleStage(stage domain.RuleStage) error { var weightSum int64 hasBaseTier := false hasZeroTier := false + tierIDs := make(map[string]struct{}, len(stage.Tiers)) for _, tier := range stage.Tiers { if tier.Stage != stage.Stage { return xerr.New(xerr.InvalidArgument, fmt.Sprintf("tier %s stage does not match %s", tier.TierID, stage.Stage)) @@ -169,6 +170,11 @@ func validateRuleStage(stage domain.RuleStage) error { if tier.TierID == "" { return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s tier id is required", stage.Stage)) } + // 同阶段奖档 ID 是 lucky_gift_stage_tiers 的主键维度;发布前拦截重复值,避免运营配置错误落到数据库唯一键后变成 opaque internal error。 + if _, exists := tierIDs[tier.TierID]; exists { + return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s tier id is duplicated: %s", stage.Stage, tier.TierID)) + } + tierIDs[tier.TierID] = struct{}{} if tier.MultiplierPPM < 0 { return xerr.New(xerr.InvalidArgument, fmt.Sprintf("%s tier multiplier is invalid", tier.TierID)) } diff --git a/services/activity-service/internal/service/luckygift/config_test.go b/services/activity-service/internal/service/luckygift/config_test.go index 5328d272..e410ed7f 100644 --- a/services/activity-service/internal/service/luckygift/config_test.go +++ b/services/activity-service/internal/service/luckygift/config_test.go @@ -57,3 +57,16 @@ func TestValidateRuleConfigRequiresExplicitZeroTier(t *testing.T) { t.Fatalf("expected stage without 0x tier to be rejected") } } + +func TestValidateRuleConfigRejectsDuplicatedStageTierID(t *testing.T) { + config := DefaultRuleConfig("hyapp", "pool_duplicate_tier") + advanced := &config.Stages[2] + duplicate := advanced.Tiers[len(advanced.Tiers)-1] + duplicate.MultiplierPPM = 10_000_000 + duplicate.BaseWeightPPM = 0 + advanced.Tiers = append(advanced.Tiers, duplicate) + + if err := validateRuleConfig(config); err == nil { + t.Fatalf("expected duplicate tier id inside one stage to be rejected") + } +}