2026-07-10 14:31:56 +08:00

159 lines
5.6 KiB
JavaScript

import { stageOptions } from "@/features/lucky-gift/constants.js";
import {
applyProbabilityMap,
correctRTPProbabilities,
decimal,
expectedRTPPercent,
formatDecimal,
probabilityTotal
} from "@/shared/utils/rtpProbability.js";
import { DEFAULT_POOL_ID } from "./api.js";
// 幸运礼物配置表单的纯领域逻辑:配置 <-> 表单换算、概率闭合校正、阶段摘要。
// 与展示层分离,弹窗组件只负责渲染和事件绑定。
export function configToForm(config = {}) {
const targetRTPPercent = formNumber(config.target_rtp_percent ?? config.targetRtpPercent);
return {
app_code: config.app_code || config.appCode || "",
control_band_percent: formNumber(config.control_band_percent ?? config.controlBandPercent),
enabled: Boolean(config.enabled),
gift_price_reference: formNumber(config.gift_price_reference ?? config.giftPriceReference),
pool_id: config.pool_id || config.poolId || DEFAULT_POOL_ID,
pool_rate_percent: formNumber(config.pool_rate_percent ?? config.poolRatePercent),
settlement_window_wager: formNumber(config.settlement_window_wager ?? config.settlementWindowWager),
stages: normalizeFormStages(config.stages, targetRTPPercent),
target_rtp_percent: targetRTPPercent
};
}
export function formToConfig(config, form) {
return {
...config,
app_code: form.app_code,
control_band_percent: numberFromForm(form.control_band_percent),
enabled: Boolean(form.enabled),
gift_price_reference: numberFromForm(form.gift_price_reference),
pool_id: form.pool_id,
pool_rate_percent: numberFromForm(form.pool_rate_percent),
settlement_window_wager: numberFromForm(form.settlement_window_wager),
stages: form.stages,
target_rtp_percent: numberFromForm(form.target_rtp_percent)
};
}
// 概率列必须整体闭合到 100% 且期望 RTP 逼近目标值,所以任何倍率/目标 RTP 改动后都重算全阶段概率,
// 不允许运营手工填概率绕过服务端的闭合校验。
export function correctAllStageProbabilities(form) {
return {
...form,
stages: (form.stages || []).map((stage) => {
const probabilityByIndex = correctRTPProbabilities(stage.tiers, form.target_rtp_percent, {
multiplier: (item) => item.multiplier
});
return {
...stage,
tiers: probabilityByIndex ? applyProbabilityMap(stage.tiers, probabilityByIndex, formatDecimal) : stage.tiers
};
})
};
}
export function stageSummary(stage, form) {
const probability = probabilityTotal(stage.tiers);
const expected = expectedRTPPercent(stage.tiers, {
multiplier: (item) => item.multiplier
});
const target = Number(form.target_rtp_percent || 0);
const band = Number(form.control_band_percent || 0);
const probabilityClosed = Math.abs(probability - 100) < 0.0001;
const rtpInBand = expected >= target - band && expected <= target + band;
const fullLabel = stageOptions.find(([key]) => key === stage.stage)?.[1] || stage.stage;
return {
expected,
ok: probabilityClosed && rtpInBand,
probability,
probabilityClosed,
rtpInBand,
stageKey: stage.stage,
stageLabel: fullLabel,
// 发布按钮不做硬拦截(服务端仍会校验),这里的状态只用于提示运营当前阶段是否可安全发布。
status: probabilityClosed && rtpInBand ? "可发布" : probabilityClosed ? "RTP 偏离" : "概率未闭合",
tabLabel: stage.stage === "normal" ? "普通" : fullLabel.replace(/阶段$/, "")
};
}
export function nextStageMultiplier(tiers = []) {
const positive = tiers.map((tier) => decimal(tier.multiplier)).filter((value) => value > 0);
if (!positive.length) {
return 1;
}
const max = Math.max(...positive);
if (max >= 5) {
return max * 2;
}
if (max >= 2) {
return 5;
}
return max + 1;
}
function normalizeFormStages(stages, targetRTPPercent = 95) {
const byStage = new Map((Array.isArray(stages) ? stages : []).map((stage) => [stage.stage, stage]));
return correctAllStageProbabilities({
stages: stageOptions.map(([stageKey]) => {
const source = byStage.get(stageKey);
const sourceTiers = Array.isArray(source?.tiers) && source.tiers.length ? source.tiers : defaultStageTiers(stageKey);
return {
stage: stageKey,
tiers: sourceTiers.map((tier) => ({
enabled: tier.enabled !== false,
highWaterOnly: Boolean(tier.high_water_only ?? tier.highWaterOnly),
multiplier: formNumber(tier.multiplier),
probabilityPercent: formNumber(tier.probability_percent ?? tier.probabilityPercent),
tierId: tier.tier_id || tier.tierId || ""
}))
};
}),
target_rtp_percent: targetRTPPercent
}).stages;
}
function defaultStageTiers(stage) {
if (stage === "advanced") {
return [
defaultTier("advanced_none", 0, 27),
defaultTier("advanced_1x", 1, 60),
defaultTier("advanced_2x", 2, 10),
defaultTier("advanced_5x", 5, 3, { high_water_only: true })
];
}
return [
defaultTier(`${stage}_none`, 0, 10),
defaultTier(`${stage}_0_5x`, 0.5, 20),
defaultTier(`${stage}_1x`, 1, 55),
defaultTier(`${stage}_2x`, 2, 15)
];
}
function defaultTier(tierID, multiplier, probabilityPercent, options = {}) {
return {
enabled: true,
highWaterOnly: Boolean(options.high_water_only),
multiplier,
probabilityPercent,
tierId: tierID
};
}
export function formNumber(value) {
const parsed = Number(value);
return Number.isFinite(parsed) ? String(parsed) : "0";
}
export function numberFromForm(value) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}