From 59fe24988624df3cf6bf4585cb6833b3f0ff7819 Mon Sep 17 00:00:00 2001 From: zhx Date: Fri, 17 Jul 2026 11:24:28 +0800 Subject: [PATCH] =?UTF-8?q?ops-center=20=E5=B9=B8=E8=BF=90=E7=A4=BC?= =?UTF-8?q?=E7=89=A9=20V2=20=E9=AB=98=E6=B0=B4=E4=BD=8D=E5=8A=A0=E6=9D=83?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 返奖与资金分配区块新增 fixed_v2 专属三字段:加权阈值/系数/恢复水位, 含帮助文案与表单校验;dynamic_v3 序列化强制归零,owner 拒绝混填。 Co-Authored-By: Claude Fable 5 --- ops-center/src/api.js | 13 +++ ops-center/src/api.test.js | 4 + .../components/LuckyGiftConfigSections.jsx | 32 ++++++++ .../src/components/LuckyGiftFieldHelp.jsx | 18 +++++ ops-center/src/configForm.js | 30 +++++-- ops-center/src/configForm.test.js | 79 +++++++++++++++++++ 6 files changed, 171 insertions(+), 5 deletions(-) diff --git a/ops-center/src/api.js b/ops-center/src/api.js index f21e859..2532592 100644 --- a/ops-center/src/api.js +++ b/ops-center/src/api.js @@ -1019,6 +1019,19 @@ function luckyGiftConfigPayload(config = {}) { : {}), user_daily_payout_cap: numberValue(config.user_daily_payout_cap ?? config.userDailyPayoutCap), user_hourly_payout_cap: numberValue(config.user_hourly_payout_cap ?? config.userHourlyPayoutCap), + // V2 高水位加权是 fixed_v2 专属;dynamic_v3 有独立水位因子,owner 会拒绝混填,这里直接归零。 + v2_high_water_boost_threshold_coins: + strategyVersion === "dynamic_v3" + ? 0 + : numberValue(config.v2_high_water_boost_threshold_coins ?? config.v2HighWaterBoostThresholdCoins), + v2_high_water_boost_factor_percent: + strategyVersion === "dynamic_v3" + ? 0 + : numberValue(config.v2_high_water_boost_factor_percent ?? config.v2HighWaterBoostFactorPercent), + v2_high_water_boost_recover_coins: + strategyVersion === "dynamic_v3" + ? 0 + : numberValue(config.v2_high_water_boost_recover_coins ?? config.v2HighWaterBoostRecoverCoins), target_rtp_percent: numberValue(config.target_rtp_percent ?? config.targetRtpPercent), }; } diff --git a/ops-center/src/api.test.js b/ops-center/src/api.test.js index 11c5b9c..6766ebf 100644 --- a/ops-center/src/api.test.js +++ b/ops-center/src/api.test.js @@ -322,6 +322,10 @@ test("saves lucky gift config through admin ops route", async () => { target_rtp_percent: 98, user_daily_payout_cap: 615_600, user_hourly_payout_cap: 34_200, + // dynamic_v3 不解释 V2 高水位加权;HTTP payload 契约要求三个字段显式归零,owner 拒绝混填。 + v2_high_water_boost_factor_percent: 0, + v2_high_water_boost_recover_coins: 0, + v2_high_water_boost_threshold_coins: 0, }); }); diff --git a/ops-center/src/components/LuckyGiftConfigSections.jsx b/ops-center/src/components/LuckyGiftConfigSections.jsx index 3df588e..9bc217e 100644 --- a/ops-center/src/components/LuckyGiftConfigSections.jsx +++ b/ops-center/src/components/LuckyGiftConfigSections.jsx @@ -295,6 +295,38 @@ export function LuckyGiftConfigSections({ appOptions, experimentMode = false, fo

发布前请核对收礼对象收益与实际送礼返币比例;不一致时,V3 真实开奖会拒绝账务结算。

+ {form.strategy_version !== "dynamic_v3" ? ( + <> +
+ + + +
+

+ V2 高水位加权:奖池水位越过阈值后,中奖返奖按系数放大(150 表示 RTP + 变为原来的 150%),直到水位跌破恢复水位才恢复正常;阈值填 0 表示关闭。 +

+ + ) : null} {form.strategy_version === "dynamic_v3" ? ( diff --git a/ops-center/src/components/LuckyGiftFieldHelp.jsx b/ops-center/src/components/LuckyGiftFieldHelp.jsx index f56b1d8..8e6a824 100644 --- a/ops-center/src/components/LuckyGiftFieldHelp.jsx +++ b/ops-center/src/components/LuckyGiftFieldHelp.jsx @@ -105,6 +105,24 @@ const FIELD_HELP = { adjust: "调高会更快释放奖池、增加正奖机会;调低则更接近正常节奏。", constraint: "必须大于 100%;放大后会重新归一化,并继续受奖池和返奖上限限制;它不会直接提高大奖触发机会。", }, + v2_high_water_boost_threshold_coins: { + summary: + "仅 V2 策略生效:本抽应进入奖池的金币先入账;入账后水位高于这个值时进入加权状态,中奖返奖金额按加权系数放大。", + adjust: "调低会更早触发加速返还;调高会让奖池积累更多后才开始放大返奖。", + constraint: "填 0 表示关闭;启用时必须大于 0,且加权系数与恢复水位都必须一并配置。", + }, + v2_high_water_boost_factor_percent: { + summary: + "加权状态下中奖返奖金额放大到多少。填 150 表示每笔中奖金额与整体 RTP 都变为原来的 150%(2 倍中奖实付 3 倍)。", + adjust: "调高会更快消化奖池积压,也会让玩家短期收益更高;调低则更温和。", + constraint: "必须大于 100% 且不超过 1000%;放大后的返奖仍受奖池支付能力限制,不会透支。", + }, + v2_high_water_boost_recover_coins: { + summary: + "退出加权的水位下沿:一旦触发加权,即使水位回落到阈值以下仍继续放大,直到水位低于这个值才恢复正常返奖。", + adjust: "调低会让加权持续更久、消化更彻底;调高会更快恢复正常节奏。", + constraint: "必须大于 0 且不高于加权阈值;与阈值相等表示不留滞回区间。", + }, recharge_boost_window_ms: { summary: "用户完成一次可验证充值后,普通开奖中非 0 倍档的机会加成持续多久。300000 毫秒等于 5 分钟。", adjust: "调大会延长充值后的加成时间;调小会让加成更快结束。", diff --git a/ops-center/src/configForm.js b/ops-center/src/configForm.js index 84d24c2..dcbaad8 100644 --- a/ops-center/src/configForm.js +++ b/ops-center/src/configForm.js @@ -97,6 +97,9 @@ const numericFields = [ "user_24h_rtp_threshold_percent", "user_daily_payout_cap", "user_hourly_payout_cap", + "v2_high_water_boost_factor_percent", + "v2_high_water_boost_recover_coins", + "v2_high_water_boost_threshold_coins", ]; const payoutCapFields = [ @@ -209,10 +212,11 @@ export function formToConfig(config, form) { strategy_version: form.strategy_version, }; for (const field of numericFields) { - output[field] = - field === "initial_pool_coins" && form.strategy_version === DYNAMIC_STRATEGY - ? 0 - : numberFromForm(form[field]); + // dynamic_v3 强制归零:隐式初始注资已废弃,V2 高水位加权字段属于 fixed_v2 专属,owner 会拒绝混填。 + const forcedZero = + form.strategy_version === DYNAMIC_STRATEGY && + (field === "initial_pool_coins" || field.startsWith("v2_high_water_boost")); + output[field] = forcedZero ? 0 : numberFromForm(form[field]); } if ( form.strategy_version === DYNAMIC_STRATEGY && @@ -357,7 +361,23 @@ export function validateLuckyGiftForm(form) { errors.push(`${stageLabel(stageKey)}期望 RTP 超出控制带宽`); } - if (form.strategy_version !== DYNAMIC_STRATEGY) return unique(errors); + if (form.strategy_version !== DYNAMIC_STRATEGY) { + // V2 高水位加权三元组:阈值 0 表示关闭且另两项必须为 0;启用时系数放大 RTP,恢复水位构成滞回下沿。 + const boostThreshold = number("v2_high_water_boost_threshold_coins"); + const boostFactor = number("v2_high_water_boost_factor_percent"); + const boostRecover = number("v2_high_water_boost_recover_coins"); + if (boostThreshold === 0) { + if (boostFactor !== 0 || boostRecover !== 0) + errors.push("关闭 V2 高水位加权时,加权系数与恢复水位必须为 0"); + } else { + if (boostThreshold < 0) errors.push("V2 高水位加权阈值不能小于 0"); + if (boostFactor <= 100 || boostFactor > 1000) + errors.push("V2 高水位加权系数必须大于 100% 且不超过 1000%"); + if (boostRecover <= 0 || boostRecover > boostThreshold) + errors.push("V2 加权恢复水位必须大于 0 且不高于加权阈值"); + } + return unique(errors); + } if (number("profit_rate_percent") < 0 || number("anchor_rate_percent") < 0) errors.push("平台盈利和主播收益比例不能小于 0"); diff --git a/ops-center/src/configForm.test.js b/ops-center/src/configForm.test.js index c5792ec..86478f3 100644 --- a/ops-center/src/configForm.test.js +++ b/ops-center/src/configForm.test.js @@ -243,6 +243,10 @@ function completeDynamicConfig() { target_rtp_percent: 98, user_daily_payout_cap: 3_000_000, user_hourly_payout_cap: 2_000_000, + // dynamic_v3 不解释 V2 高水位加权;序列化契约要求这三个字段始终显式归零回传。 + v2_high_water_boost_factor_percent: 0, + v2_high_water_boost_recover_coins: 0, + v2_high_water_boost_threshold_coins: 0, }; } @@ -289,3 +293,78 @@ function tier(tierId, multiplier, probabilityPercent) { tier_id: tierId, }; } + +describe("ops center lucky gift v2 high-water boost", () => { + const fixedForm = (overrides = {}) => + configToForm({ + app_code: "yumi", + control_band_percent: 1, + enabled: true, + gift_price_reference: 100, + normal_max_equivalent_draws: 20_000, + novice_max_equivalent_draws: 2_000, + pool_id: "legacy", + pool_rate_percent: 96, + settlement_window_wager: 1_000_000, + stages: fixedStages(), + strategy_version: "fixed_v2", + target_rtp_percent: 95, + ...overrides, + }); + + test("round-trips the boost triple for fixed_v2", () => { + const form = fixedForm({ + v2_high_water_boost_factor_percent: 150, + v2_high_water_boost_recover_coins: 1_000_000, + v2_high_water_boost_threshold_coins: 2_000_000, + }); + expect(form.v2_high_water_boost_threshold_coins).toBe("2000000"); + expect(form.v2_high_water_boost_factor_percent).toBe("150"); + expect(form.v2_high_water_boost_recover_coins).toBe("1000000"); + expect(validateLuckyGiftForm(form)).toEqual([]); + const config = formToConfig({}, form); + expect(config.v2_high_water_boost_threshold_coins).toBe(2_000_000); + expect(config.v2_high_water_boost_factor_percent).toBe(150); + expect(config.v2_high_water_boost_recover_coins).toBe(1_000_000); + }); + + test("legacy fixed_v2 snapshots without the fields stay disabled and valid", () => { + const form = fixedForm(); + expect(form.v2_high_water_boost_threshold_coins).toBe("0"); + expect(validateLuckyGiftForm(form)).toEqual([]); + }); + + test("rejects partial or out-of-range boost triples", () => { + const halfConfigured = fixedForm({ v2_high_water_boost_factor_percent: 150 }); + expect(validateLuckyGiftForm(halfConfigured)).toContain("关闭 V2 高水位加权时,加权系数与恢复水位必须为 0"); + + const factorTooLow = fixedForm({ + v2_high_water_boost_factor_percent: 100, + v2_high_water_boost_recover_coins: 1_000_000, + v2_high_water_boost_threshold_coins: 2_000_000, + }); + expect(validateLuckyGiftForm(factorTooLow)).toContain("V2 高水位加权系数必须大于 100% 且不超过 1000%"); + + const recoverAboveThreshold = fixedForm({ + v2_high_water_boost_factor_percent: 150, + v2_high_water_boost_recover_coins: 3_000_000, + v2_high_water_boost_threshold_coins: 2_000_000, + }); + expect(validateLuckyGiftForm(recoverAboveThreshold)).toContain("V2 加权恢复水位必须大于 0 且不高于加权阈值"); + }); + + test("switching to dynamic_v3 zeroes the fixed_v2-only fields on serialization", () => { + const boosted = fixedForm({ + v2_high_water_boost_factor_percent: 150, + v2_high_water_boost_recover_coins: 1_000_000, + v2_high_water_boost_threshold_coins: 2_000_000, + }); + const dynamic = applyDynamicStrategyDefaults(boosted); + const config = formToConfig({}, dynamic); + // owner 拒绝 dynamic_v3 携带 v2 加权字段;序列化必须归零,不能把 fixed_v2 的残值带进新策略版本。 + expect(config.v2_high_water_boost_threshold_coins).toBe(0); + expect(config.v2_high_water_boost_factor_percent).toBe(0); + expect(config.v2_high_water_boost_recover_coins).toBe(0); + expect(validateLuckyGiftForm(dynamic)).toEqual([]); + }); +});