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; }