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_daily_payout_cap: numberValue(config.user_daily_payout_cap ?? config.userDailyPayoutCap),
|
||||||
user_hourly_payout_cap: numberValue(config.user_hourly_payout_cap ?? config.userHourlyPayoutCap),
|
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),
|
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,
|
target_rtp_percent: 98,
|
||||||
user_daily_payout_cap: 615_600,
|
user_daily_payout_cap: 615_600,
|
||||||
user_hourly_payout_cap: 34_200,
|
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">
|
<p className="ops-field-note">
|
||||||
发布前请核对收礼对象收益与实际送礼返币比例;不一致时,V3 真实开奖会拒绝账务结算。
|
发布前请核对收礼对象收益与实际送礼返币比例;不一致时,V3 真实开奖会拒绝账务结算。
|
||||||
</p>
|
</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>
|
</section>
|
||||||
|
|
||||||
{form.strategy_version === "dynamic_v3" ? (
|
{form.strategy_version === "dynamic_v3" ? (
|
||||||
|
|||||||
@ -105,6 +105,24 @@ const FIELD_HELP = {
|
|||||||
adjust: "调高会更快释放奖池、增加正奖机会;调低则更接近正常节奏。",
|
adjust: "调高会更快释放奖池、增加正奖机会;调低则更接近正常节奏。",
|
||||||
constraint: "必须大于 100%;放大后会重新归一化,并继续受奖池和返奖上限限制;它不会直接提高大奖触发机会。",
|
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: {
|
recharge_boost_window_ms: {
|
||||||
summary: "用户完成一次可验证充值后,普通开奖中非 0 倍档的机会加成持续多久。300000 毫秒等于 5 分钟。",
|
summary: "用户完成一次可验证充值后,普通开奖中非 0 倍档的机会加成持续多久。300000 毫秒等于 5 分钟。",
|
||||||
adjust: "调大会延长充值后的加成时间;调小会让加成更快结束。",
|
adjust: "调大会延长充值后的加成时间;调小会让加成更快结束。",
|
||||||
|
|||||||
@ -97,6 +97,9 @@ const numericFields = [
|
|||||||
"user_24h_rtp_threshold_percent",
|
"user_24h_rtp_threshold_percent",
|
||||||
"user_daily_payout_cap",
|
"user_daily_payout_cap",
|
||||||
"user_hourly_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 = [
|
const payoutCapFields = [
|
||||||
@ -209,10 +212,11 @@ export function formToConfig(config, form) {
|
|||||||
strategy_version: form.strategy_version,
|
strategy_version: form.strategy_version,
|
||||||
};
|
};
|
||||||
for (const field of numericFields) {
|
for (const field of numericFields) {
|
||||||
output[field] =
|
// dynamic_v3 强制归零:隐式初始注资已废弃,V2 高水位加权字段属于 fixed_v2 专属,owner 会拒绝混填。
|
||||||
field === "initial_pool_coins" && form.strategy_version === DYNAMIC_STRATEGY
|
const forcedZero =
|
||||||
? 0
|
form.strategy_version === DYNAMIC_STRATEGY &&
|
||||||
: numberFromForm(form[field]);
|
(field === "initial_pool_coins" || field.startsWith("v2_high_water_boost"));
|
||||||
|
output[field] = forcedZero ? 0 : numberFromForm(form[field]);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
form.strategy_version === DYNAMIC_STRATEGY &&
|
form.strategy_version === DYNAMIC_STRATEGY &&
|
||||||
@ -357,7 +361,23 @@ export function validateLuckyGiftForm(form) {
|
|||||||
errors.push(`${stageLabel(stageKey)}期望 RTP 超出控制带宽`);
|
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)
|
if (number("profit_rate_percent") < 0 || number("anchor_rate_percent") < 0)
|
||||||
errors.push("平台盈利和主播收益比例不能小于 0");
|
errors.push("平台盈利和主播收益比例不能小于 0");
|
||||||
|
|||||||
@ -243,6 +243,10 @@ function completeDynamicConfig() {
|
|||||||
target_rtp_percent: 98,
|
target_rtp_percent: 98,
|
||||||
user_daily_payout_cap: 3_000_000,
|
user_daily_payout_cap: 3_000_000,
|
||||||
user_hourly_payout_cap: 2_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,
|
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