diff --git a/ops-center/src/configForm.js b/ops-center/src/configForm.js index 90dc23e..316b750 100644 --- a/ops-center/src/configForm.js +++ b/ops-center/src/configForm.js @@ -284,6 +284,9 @@ export function validateLuckyGiftForm(form) { ) errors.push("等价抽数阶段门槛不正确"); + const jackpotMultipliers = + form.strategy_version === DYNAMIC_STRATEGY ? new Set(multiplierListFromForm(form.jackpot_multipliers)) : null; + // 阶段概率与 RTP 是两代策略共用的 owner 校验,fixed_v2 也不能跳过。 for (const stageKey of ["novice", "normal", "advanced"]) { const stage = form.stages?.find((item) => item.stage === stageKey); @@ -302,6 +305,19 @@ export function validateLuckyGiftForm(form) { errors.push(`${stageLabel(stageKey)}启用奖档概率必须大于 0`); if (enabled.some((tier) => Number(tier.multiplier) >= 10 && !tier.highWaterOnly)) errors.push(`${stageLabel(stageKey)}10x 及以上奖档必须限制为高水位`); + const jackpotStageMultipliers = + jackpotMultipliers && + enabled + .filter( + (tier) => + jackpotMultipliers.has(numberFromForm(tier.multiplier)) && + percentToPPM(tier.probabilityPercent) > 0, + ) + .map((tier) => formatDecimal(numberFromForm(tier.multiplier))); + if (jackpotStageMultipliers?.length) { + // dynamic_v3 的大奖倍率只能走大奖补偿路径;普通阶段再给正概率会绕过 owner 的双 RTP 门槛。 + errors.push(`${stageLabel(stageKey)}普通奖档不能包含大奖倍率:${unique(jackpotStageMultipliers).join("x、")}x`); + } const expected = expectedRTPPercent(enabled, { multiplier: (item) => item.multiplier }); if ( expected < number("target_rtp_percent") - number("control_band_percent") || @@ -333,11 +349,11 @@ export function validateLuckyGiftForm(form) { const jackpotTokens = String(form.jackpot_multipliers || "") .split(/[,,\s]+/) .filter(Boolean); - const jackpotMultipliers = multiplierListFromForm(form.jackpot_multipliers); + const jackpotMultiplierValues = multiplierListFromForm(form.jackpot_multipliers); if ( - jackpotTokens.length !== jackpotMultipliers.length || - !jackpotMultipliers.length || - jackpotMultipliers.some((value, index) => value <= 1 || (index > 0 && value <= jackpotMultipliers[index - 1])) + jackpotTokens.length !== jackpotMultiplierValues.length || + !jackpotMultiplierValues.length || + jackpotMultiplierValues.some((value, index) => value <= 1 || (index > 0 && value <= jackpotMultiplierValues[index - 1])) ) { errors.push("大奖倍率必须大于 1x 并严格递增"); } diff --git a/ops-center/src/configForm.test.js b/ops-center/src/configForm.test.js index 971a489..1c302dd 100644 --- a/ops-center/src/configForm.test.js +++ b/ops-center/src/configForm.test.js @@ -70,7 +70,6 @@ describe("ops center lucky gift dynamic_v3 form", () => { const enabled = { ...draft, enabled: true }; const errors = validateLuckyGiftForm(enabled); - expect(errors).toContain("启用前必须填写用户日累计消费触发门槛(金币)"); expect(errors).toContain("启用前普通阶段至少一个最低充值门槛必须大于 0"); expect(errors).toContain("高阶充值门槛必须逐维不低于正常阶段且至少一项更高"); expect(errors).toContain("启用前必须填写单次返奖上限"); @@ -151,6 +150,32 @@ describe("ops center lucky gift dynamic_v3 form", () => { expect(errors).toContain("单次返奖上限不能小于 0"); expect(errors).toContain("充值阶段门槛不能小于 0"); }); + + test("rejects jackpot multipliers that still have ordinary stage probability", () => { + const form = configToForm(completeDynamicConfig()); + const errors = validateLuckyGiftForm({ + ...form, + stages: form.stages.map((stage) => + stage.stage === "normal" + ? { + ...stage, + tiers: [ + ...stage.tiers, + { + enabled: true, + highWaterOnly: true, + multiplier: "200", + probabilityPercent: "0.01", + tierId: "normal_200x", + }, + ], + } + : stage, + ), + }); + + expect(errors).toContain("正常阶段普通奖档不能包含大奖倍率:200x"); + }); }); function completeDynamicConfig() {