ops-center 幸运礼物 V2 高水位加权配置项
返奖与资金分配区块新增 fixed_v2 专属三字段:加权阈值/系数/恢复水位, 含帮助文案与表单校验;dynamic_v3 序列化强制归零,owner 拒绝混填。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
f120ffbc9b
commit
59fe249886
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -295,6 +295,38 @@ export function LuckyGiftConfigSections({ appOptions, experimentMode = false, fo
|
||||
<p className="ops-field-note">
|
||||
发布前请核对收礼对象收益与实际送礼返币比例;不一致时,V3 真实开奖会拒绝账务结算。
|
||||
</p>
|
||||
{form.strategy_version !== "dynamic_v3" ? (
|
||||
<>
|
||||
<div className="ops-form-grid">
|
||||
<NumberField
|
||||
form={form}
|
||||
label="高水位加权阈值(金币)"
|
||||
name="v2_high_water_boost_threshold_coins"
|
||||
required={false}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<NumberField
|
||||
form={form}
|
||||
label="高水位加权系数 (%)"
|
||||
name="v2_high_water_boost_factor_percent"
|
||||
required={false}
|
||||
step="0.01"
|
||||
onChange={onChange}
|
||||
/>
|
||||
<NumberField
|
||||
form={form}
|
||||
label="加权恢复水位(金币)"
|
||||
name="v2_high_water_boost_recover_coins"
|
||||
required={false}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
<p className="ops-field-note">
|
||||
V2 高水位加权:奖池水位越过阈值后,中奖返奖按系数放大(150 表示 RTP
|
||||
变为原来的 150%),直到水位跌破恢复水位才恢复正常;阈值填 0 表示关闭。
|
||||
</p>
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{form.strategy_version === "dynamic_v3" ? (
|
||||
|
||||
@ -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: "调大会延长充值后的加成时间;调小会让加成更快结束。",
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user