From 307a942b8e0211a4bd390956e97162c2fdf19176 Mon Sep 17 00:00:00 2001 From: zhenhui <1276357500@qq.com> Date: Wed, 3 Jun 2026 17:23:13 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BC=98=E5=8C=96=E6=8A=BD=E5=A5=96=E5=88=B8?= =?UTF-8?q?=E7=9A=84=E6=8A=BD=E5=A5=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../langs/en/dice/lottery_pool_config.json | 4 +- .../src/locales/langs/en/dice/reward.json | 11 ++- .../langs/zh/dice/lottery_pool_config.json | 4 +- .../src/locales/langs/zh/dice/reward.json | 11 ++- .../dice/lottery_pool_config/index/index.vue | 1 + .../index/modules/weight-test-dialog.vue | 56 +++++++++----- server/app/api/logic/PlayStartLogic.php | 30 +++++--- .../reward/DiceRewardController.php | 2 +- .../reward_config_record/WeightTestRunner.php | 77 ++++++++++++++----- 9 files changed, 136 insertions(+), 60 deletions(-) diff --git a/saiadmin-artd/src/locales/langs/en/dice/lottery_pool_config.json b/saiadmin-artd/src/locales/langs/en/dice/lottery_pool_config.json index 591cfb8..b846d73 100644 --- a/saiadmin-artd/src/locales/langs/en/dice/lottery_pool_config.json +++ b/saiadmin-artd/src/locales/langs/en/dice/lottery_pool_config.json @@ -8,6 +8,7 @@ "poolType": "Pool Type", "placeholderPoolType": "Please select pool type", "poolTypeNormal": "Normal", + "poolTypeFree": "Free", "poolTypeKill": "Kill", "poolTypeT1": "T1 High", "safetyLine": "Safety Line", @@ -25,7 +26,7 @@ "realtime": "Live", "profitCalcHint": "Accumulated on name=default (Normal) pool: paid += win_coin − paid_amount (ante×1); free += win_coin. Compared with safety line to decide paid-draw kill switch. Refreshes every 2s while open.", "tierRuleTitle": "Paid draw tier rule", - "tierRuleContent": "Compares default pool profit_amount (not per-player profit). Below safety line or kill off: use player T*_weight; at or above safety line with kill on: use killScore pool T*_weight. Free draws always use killScore weights (safety line N/A).", + "tierRuleContent": "Compares default pool profit_amount (not per-player profit). Below safety line or kill off: paid uses player T*_weight; at/above safety line with kill on: paid uses killScore pool. Free draws always use channel name=free pool weights (fallback default if missing); safety line N/A.", "enableKillScore": "Enable kill score", "killScoreWeights": "Kill weights (killScore)", "killWeightNote": "Edit killScore (Force Kill) row in the list for kill weights. This dialog only configures default pool safety line and kill switch.", @@ -57,6 +58,7 @@ "placeholderName": "Please enter name", "placeholderPoolType": "Please select pool type", "poolTypeNormal": "Normal", + "poolTypeFree": "Free", "poolTypeKill": "Force Kill", "poolTypeT1": "T1 High Rate" }, diff --git a/saiadmin-artd/src/locales/langs/en/dice/reward.json b/saiadmin-artd/src/locales/langs/en/dice/reward.json index c494ec6..cf260da 100644 --- a/saiadmin-artd/src/locales/langs/en/dice/reward.json +++ b/saiadmin-artd/src/locales/langs/en/dice/reward.json @@ -54,11 +54,14 @@ "weightTest": { "title": "One-Click Weight Test", "alertTitle": "Bonus pool logic", - "alertBody": "Test mode is non-kill by default. You can enable kill mode below with switch + safety line: once simulated player cumulative profit reaches the line, paid draws switch to killScore.", + "alertBody": "Test mode is non-kill by default. When test kill mode is enabled, rules match production: compare default pool profit_amount to the safety line, with kill_enabled on and a killScore config present.", "chainModeHint": "Simulation: set paid spin counts only (CW/CCW). If a paid draw hits “play again” (or T5), the next draw is free with the same ante, lottery type free, paid amount 0. Free-draw tier odds are configured below (including chained free plays).", - "killModeHint": "When test kill mode is enabled: use simulated player cumulative profit as trigger; once cumulative profit >= safety line, subsequent paid draws use killScore. Free draws still follow the configured free settings.", + "killModeHint": "When test kill mode is on: start from default pool profit_amount and accumulate each spin (paid: win_coin - paid_amount; free: win_coin). Once profit >= safety line and kill_enabled is on, subsequent paid draws use killScore; free draws still use the name=free pool.", "labelKillModeEnabled": "Enable test kill mode", - "labelTestSafetyLine": "Test safety line", + "labelTestSafetyLine": "default safety line", + "killEnabledOn": "kill on", + "killEnabledOff": "kill off", + "poolProfitPrefix": "pool profit ", "sectionPaid": "Paid draws", "sectionFreeAfterPlayAgain": "Free draw tier odds (after play-again)", "tierProbHintFreeChain": "When using custom tier odds: T1–T5 below apply when a free draw runs (tier roll; combined with dice_reward row weights).", @@ -69,7 +72,7 @@ "labelAnte": "Ante", "placeholderAnte": "Select ante config", "placeholderPaidPool": "Leave empty for custom tier odds below (default: default)", - "placeholderFreePool": "Leave empty for custom tier odds below (default: killScore)", + "placeholderFreePool": "Leave empty for custom tier odds below (default: free pool)", "tierProbHint": "Custom tier odds (T1–T5), each 0–100%, sum of five must not exceed 100%", "tierFieldLabel": "Tier {tier} (%)", "tierSumError": "Current sum of five tiers is {sum}%, cannot exceed 100%", diff --git a/saiadmin-artd/src/locales/langs/zh/dice/lottery_pool_config.json b/saiadmin-artd/src/locales/langs/zh/dice/lottery_pool_config.json index d70e136..e319ac0 100644 --- a/saiadmin-artd/src/locales/langs/zh/dice/lottery_pool_config.json +++ b/saiadmin-artd/src/locales/langs/zh/dice/lottery_pool_config.json @@ -8,6 +8,7 @@ "poolType": "奖池类型", "placeholderPoolType": "请选择奖池类型", "poolTypeNormal": "正常", + "poolTypeFree": "免费", "poolTypeKill": "强制杀猪", "poolTypeT1": "T1高倍率", "safetyLine": "安全线", @@ -25,7 +26,7 @@ "realtime": "实时", "profitCalcHint": "累计在 name=default(正常)奖池上:付费每局 += win_coin − paid_amount(ante×1);免费每局 += win_coin。用于与安全线比较,判定付费抽奖是否切换杀分。弹窗打开期间每 2 秒自动刷新。", "tierRuleTitle": "付费抽奖档位规则", - "tierRuleContent": "比较对象为 default 奖池的 profit_amount(非单个玩家盈利)。当 profit_amount 低于安全线或未开启杀分时,按玩家 T*_weight 抽档;当 profit_amount 高于或等于安全线且已开启杀分时,按 killScore 奖池的 T*_weight 抽档。免费抽奖始终按 killScore 权重(与安全线无关)。", + "tierRuleContent": "比较对象为 default 奖池的 profit_amount(非单个玩家盈利)。当 profit_amount 低于安全线或未开启杀分时,付费按玩家 T*_weight 抽档;当 profit_amount 高于或等于安全线且已开启杀分时,付费按 killScore 奖池抽档。免费抽奖始终按本渠道 name=free 奖池权重(无 free 时回退 default),与安全线无关。", "enableKillScore": "开启杀分", "killScoreWeights": "杀分权重(killScore)", "killWeightNote": "杀分权重请在列表中编辑 name=killScore(强制杀猪)记录;本弹窗仅配置 default 奖池的安全线与杀分开关。", @@ -57,6 +58,7 @@ "placeholderName": "请输入名称", "placeholderPoolType": "请选择奖池类型", "poolTypeNormal": "正常", + "poolTypeFree": "免费", "poolTypeKill": "强制杀猪", "poolTypeT1": "T1高倍率" }, diff --git a/saiadmin-artd/src/locales/langs/zh/dice/reward.json b/saiadmin-artd/src/locales/langs/zh/dice/reward.json index 8ed53a1..f04dfb7 100644 --- a/saiadmin-artd/src/locales/langs/zh/dice/reward.json +++ b/saiadmin-artd/src/locales/langs/zh/dice/reward.json @@ -54,11 +54,14 @@ "weightTest": { "title": "一键测试权重", "alertTitle": "彩金池逻辑说明", - "alertBody": "测试模式默认不启用杀分切换;可通过下方“杀分开关 + 安全线”在测试内启用:当模拟玩家累计盈利达到安全线后,付费抽奖切换到 killScore。", + "alertBody": "测试模式默认不启用杀分切换;开启「测试内杀分」后,判定规则与线上一致:以 default 彩金池累计盈利(profit_amount)对比安全线,且需 kill_enabled=开启、存在 killScore 配置。", "chainModeHint": "模拟方式:只配置付费抽奖次数(顺/逆时针)。付费抽到「再来一次」或 T5 时,下一局自动为免费抽奖,底注与触发局相同,抽奖类型记为免费、付费金额记为 0。免费抽奖的档位概率由下方「免费抽奖」配置决定(含通过再来一次触发的后续免费局)。", - "killModeHint": "杀分开关开启后:以“模拟玩家累计盈利”作为判定值;当累计盈利 >= 安全线时,后续付费抽奖按 killScore 配置抽取;免费抽奖仍按“免费抽奖配置”执行。", + "killModeHint": "杀分开关开启后:从 default 奖池当前 profit_amount 起步,测试内逐局累加(付费=win_coin-paid_amount,免费=win_coin);当累计盈利 ≥ 安全线且 kill_enabled 开启时,后续付费抽奖切 killScore;免费抽奖仍走 name=free 奖池。", "labelKillModeEnabled": "开启测试内杀分", - "labelTestSafetyLine": "测试安全线", + "labelTestSafetyLine": "default 安全线", + "killEnabledOn": "杀分已开启", + "killEnabledOff": "杀分已关闭", + "poolProfitPrefix": "当前池盈利 ", "sectionPaid": "付费抽奖", "sectionFreeAfterPlayAgain": "免费抽奖(再来一次后的档位概率)", "tierProbHintFreeChain": "当使用自定义档位时:以下为「免费抽奖」时 T1~T5 的档位概率(仅在有免费局时参与摇档,与 dice_reward 格子权重共同决定结果)。", @@ -69,7 +72,7 @@ "labelAnte": "底注", "placeholderAnte": "请选择底注配置", "placeholderPaidPool": "不选则下方自定义档位概率(默认 default)", - "placeholderFreePool": "不选则下方自定义档位概率(默认 killScore)", + "placeholderFreePool": "不选则下方自定义档位概率(默认 free 免费奖池)", "tierProbHint": "自定义档位概率(T1~T5),每档 0-100%,五档之和不能超过 100%", "tierFieldLabel": "档位 {tier}(%)", "tierSumError": "当前五档之和为 {sum}%,不能超过 100%", diff --git a/saiadmin-artd/src/views/plugin/dice/lottery_pool_config/index/index.vue b/saiadmin-artd/src/views/plugin/dice/lottery_pool_config/index/index.vue index e515539..b9f2a11 100644 --- a/saiadmin-artd/src/views/plugin/dice/lottery_pool_config/index/index.vue +++ b/saiadmin-artd/src/views/plugin/dice/lottery_pool_config/index/index.vue @@ -91,6 +91,7 @@ const typeFormatter = (row: Record) => { const n = String(row.name ?? '') if (n === 'default') return t('page.search.poolTypeNormal') + if (n === 'free') return t('page.search.poolTypeFree') if (n === 'killScore') return t('page.search.poolTypeKill') if (n === 'up') return t('page.search.poolTypeT1') return n || '-' diff --git a/saiadmin-artd/src/views/plugin/dice/reward/index/modules/weight-test-dialog.vue b/saiadmin-artd/src/views/plugin/dice/reward/index/modules/weight-test-dialog.vue index bd316c3..9b687fa 100644 --- a/saiadmin-artd/src/views/plugin/dice/reward/index/modules/weight-test-dialog.vue +++ b/saiadmin-artd/src/views/plugin/dice/reward/index/modules/weight-test-dialog.vue @@ -44,14 +44,11 @@ - - + @@ -221,17 +218,27 @@ free_tier_weights: { T1: 20, T2: 20, T3: 20, T4: 20, T5: 20 } as Record, paid_s_count: 100, paid_n_count: 100, - kill_mode_enabled: false, - test_safety_line: 5000 + kill_mode_enabled: false + }) + const defaultPoolSafetyLineText = computed(() => { + const info = defaultPoolInfo.value + if (!info) { + return '-' + } + const killText = info.kill_enabled === 1 + ? t('page.weightTest.killEnabledOn') + : t('page.weightTest.killEnabledOff') + return `${info.safety_line}(${killText},${t('page.weightTest.poolProfitPrefix')}${info.profit_amount})` }) const lotteryOptions = ref>([]) const paidLotteryOptions = computed(() => lotteryOptions.value.filter((r) => r.name === 'default') ) const freeLotteryOptions = computed(() => { - const list = lotteryOptions.value.filter((r) => r.name === 'killScore') - return list.length > 0 ? list : lotteryOptions.value + const list = lotteryOptions.value.filter((r) => r.name === 'free') + return list.length > 0 ? list : lotteryOptions.value.filter((r) => r.name === 'default') }) + const defaultPoolInfo = ref<{ safety_line: number; kill_enabled: number; profit_amount: number } | null>(null) const running = ref(false) function onClose() { @@ -321,6 +328,19 @@ } } + async function loadDefaultPoolInfo() { + try { + const pool = await lotteryPoolApi.getCurrentPool(resolveDeptParams()) + defaultPoolInfo.value = { + safety_line: Number(pool?.safety_line ?? 0), + kill_enabled: Number(pool?.kill_enabled ?? 1), + profit_amount: Number(pool?.profit_amount ?? 0) + } + } catch { + defaultPoolInfo.value = null + } + } + async function loadLotteryOptions() { try { const list = await lotteryPoolApi.getOptions(resolveDeptParams()) @@ -332,9 +352,9 @@ if (normal) { form.paid_lottery_config_id = normal.id } - const kill = list.find((r: { name?: string }) => r.name === 'killScore') - if (kill) { - form.free_lottery_config_id = kill.id + const freePool = list.find((r: { name?: string }) => r.name === 'free') + if (freePool) { + form.free_lottery_config_id = freePool.id } else if (list.length > 0) { form.free_lottery_config_id = list[0].id } @@ -353,7 +373,7 @@ free_n_count: 0, chain_free_mode: true, kill_mode_enabled: form.kill_mode_enabled, - test_safety_line: form.test_safety_line + test_safety_line: defaultPoolInfo.value?.safety_line ?? 0 } if (form.paid_lottery_config_id != null) { payload.paid_lottery_config_id = form.paid_lottery_config_id @@ -382,10 +402,6 @@ ElMessage.warning(t('page.weightTest.warnPaidSpins')) return false } - if (form.kill_mode_enabled && (form.test_safety_line == null || form.test_safety_line < 0)) { - ElMessage.warning(t('page.weightTest.warnTestSafetyLine')) - return false - } const needPaidTier = form.paid_lottery_config_id == null const needFreeTier = form.free_lottery_config_id == null if (needPaidTier) { @@ -437,6 +453,7 @@ if (v) { void loadAnteOptions() void loadLotteryOptions() + void loadDefaultPoolInfo() } else { onClose() } @@ -448,6 +465,7 @@ if (visible.value) { void loadAnteOptions() void loadLotteryOptions() + void loadDefaultPoolInfo() } } ) diff --git a/server/app/api/logic/PlayStartLogic.php b/server/app/api/logic/PlayStartLogic.php index 6aa7b26..234f420 100644 --- a/server/app/api/logic/PlayStartLogic.php +++ b/server/app/api/logic/PlayStartLogic.php @@ -118,7 +118,8 @@ class PlayStartLogic } $configType0 = DiceLotteryPoolConfig::where('name', 'default')->where('dept_id', $configDeptId)->find(); - $configType1 = DiceLotteryPoolConfig::where('name', 'killScore')->where('dept_id', $configDeptId)->find(); + $configKill = DiceLotteryPoolConfig::where('name', 'killScore')->where('dept_id', $configDeptId)->find(); + $configFree = DiceLotteryPoolConfig::where('name', 'free')->where('dept_id', $configDeptId)->find(); if (!$configType0) { throw new ApiException('Lottery pool config not found (name=default required)'); } @@ -132,17 +133,28 @@ class PlayStartLogic } // 彩金池累计盈利:用于判断是否触发杀分(不再依赖单个玩家累计盈利) - // 该值来自 dice_lottery_pool_config.profit_amount + // 该值来自 dice_lottery_pool_config.profit_amount(default 奖池) $poolProfitTotal = $configType0->profit_amount ?? 0; $safetyLine = (int) ($configType0->safety_line ?? 0); $killEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1; - // 盈利>=安全线且开启杀分:付费/免费都用 killScore;盈利<安全线:付费用玩家权重,免费用 killScore(无则用 default) - // 记录 lottery_config_id:用池权重时记对应池,付费用玩家权重时记 default - $usePoolWeights = ($ticketType === self::LOTTERY_TYPE_PAID && $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null) - || ($ticketType === self::LOTTERY_TYPE_FREE); - $config = $usePoolWeights - ? (($ticketType === self::LOTTERY_TYPE_FREE && $configType1 === null) ? $configType0 : $configType1) - : $configType0; + + $usePaidKill = $ticketType === self::LOTTERY_TYPE_PAID + && $killEnabled + && $poolProfitTotal >= $safetyLine + && $configKill !== null; + + if ($ticketType === self::LOTTERY_TYPE_FREE) { + // 免费抽奖券:使用本渠道 name=free 奖池档位权重;无 free 时回退 default + $config = $configFree ?? $configType0; + $usePoolWeights = true; + } elseif ($usePaidKill) { + $config = $configKill; + $usePoolWeights = true; + } else { + // 付费未触发杀分:按玩家 T*_weight 抽档,lottery_config_id 记 default + $config = $configType0; + $usePoolWeights = false; + } // 按档位 T1-T5 抽取后,从 DiceReward 表按当前方向取该档位数据,再按 weight 抽取一条得到 grid_number $rewardInstance = DiceReward::getCachedInstance($configDeptId); diff --git a/server/app/dice/controller/reward/DiceRewardController.php b/server/app/dice/controller/reward/DiceRewardController.php index e0e5fd8..3039d7d 100644 --- a/server/app/dice/controller/reward/DiceRewardController.php +++ b/server/app/dice/controller/reward/DiceRewardController.php @@ -96,7 +96,7 @@ class DiceRewardController extends BaseController * 参数:lottery_config_id 可选;paid_tier_weights / free_tier_weights 自定义档位; * paid_s_count, paid_n_count * chain_free_mode=1:仅按付费次数模拟;付费抽到再来一次/T5 则在队列中插入免费局(同底注、lottery_type=免费、paid_amount=0) - * kill_mode_enabled=1:测试内启用杀分;当模拟玩家累计盈利达到 test_safety_line 后,付费抽奖切到 killScore + * kill_mode_enabled=1:测试内启用杀分;规则与线上一致(default.profit_amount + safety_line + kill_enabled) */ #[Permission('一键测试权重', 'dice:reward:index:startWeightTest')] public function startWeightTest(Request $request): Response diff --git a/server/app/dice/logic/reward_config_record/WeightTestRunner.php b/server/app/dice/logic/reward_config_record/WeightTestRunner.php index 463f62e..d40e048 100644 --- a/server/app/dice/logic/reward_config_record/WeightTestRunner.php +++ b/server/app/dice/logic/reward_config_record/WeightTestRunner.php @@ -15,7 +15,10 @@ use support\think\Db; /** * 一键测试权重:单进程后台执行模拟摇色子,写入 dice_play_record_test 并更新 dice_reward_config_record 进度 - * 支持测试内杀分:当模拟玩家累计盈利达到安全线后,付费抽奖切换到 killScore + * 抽奖规则与 PlayStartLogic 一致: + * - 付费未杀分:按模拟玩家档位权重抽档,lottery_config_id 记 default + * - 付费杀分:default.profit_amount + safety_line + kill_enabled 满足后切 killScore + * - 免费券:name=free 奖池(无则 default),排除 5/30 豹子 */ class WeightTestRunner { @@ -72,7 +75,8 @@ class WeightTestRunner DiceRewardConfig::clearRequestInstance(); $configType0 = DiceLotteryPoolConfig::findByNameForDept('default', $deptId); - $configType1 = DiceLotteryPoolConfig::findByNameForDept('killScore', $deptId); + $configKill = DiceLotteryPoolConfig::findByNameForDept('killScore', $deptId); + $configFree = DiceLotteryPoolConfig::findByNameForDept('free', $deptId); if (!$configType0) { $this->markFailed($recordId, '彩金池配置 name=default 不存在(当前渠道)'); return; @@ -92,9 +96,9 @@ class WeightTestRunner if (!$paidPoolConfig || AdminScopeHelper::normalizeRecordDeptId($paidPoolConfig->dept_id ?? null) !== $deptId) { $paidPoolConfig = $configType0; } - $freePoolConfig = $freePoolConfigId > 0 ? DiceLotteryPoolConfig::find($freePoolConfigId) : $configType1; + $freePoolConfig = $freePoolConfigId > 0 ? DiceLotteryPoolConfig::find($freePoolConfigId) : $configFree; if (!$freePoolConfig || AdminScopeHelper::normalizeRecordDeptId($freePoolConfig->dept_id ?? null) !== $deptId) { - $freePoolConfig = $configType1 ?: $configType0; + $freePoolConfig = $configFree ?: $configType0; } if ($paidTierWeightsCustom !== null && array_sum($paidTierWeightsCustom) <= 0) { @@ -111,13 +115,14 @@ class WeightTestRunner DiceReward::clearRequestInstance(); $killModeEnabled = (int) ($record->kill_mode_enabled ?? 0) === 1; - $testSafetyLine = (int) ($record->test_safety_line ?? 5000); - if ($testSafetyLine < 0) { - $testSafetyLine = 0; - } + $safetyLine = (int) ($configType0->safety_line ?? 0); + $dbKillEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1; - // 测试内“玩家累计盈利”:用于控制付费局是否切换杀分 - $playerProfitTotal = 0.0; + // 彩金池累计盈利:与线上一致,从 default.profit_amount 起步并在测试内逐局累加 + $poolProfitTotal = (float) ($configType0->profit_amount ?? 0); + + // 付费未杀分时的模拟玩家档位权重(自定义 > 快照 > 兜底奖池) + $paidPlayerWeights = $paidTierWeightsCustom ?? $this->resolveTierWeightsSnapshot($record, 'paid'); $playLogic = new PlayStartLogic(); $resultCounts = []; @@ -133,14 +138,16 @@ class WeightTestRunner $paidS, $paidN, $ante, + $configType0, $paidPoolConfig, $freePoolConfig, - $configType1, - $paidTierWeightsCustom, + $configKill, + $paidPlayerWeights, $freeTierWeightsCustom, $killModeEnabled, - $testSafetyLine, - $playerProfitTotal, + $safetyLine, + $dbKillEnabled, + $poolProfitTotal, $resultCounts, $tierCounts, $buffer, @@ -172,14 +179,16 @@ class WeightTestRunner int $paidS, int $paidN, int $ante, + $defaultPoolConfig, $paidPoolConfig, $freePoolConfig, $killPoolConfig, - ?array $paidTierWeightsCustom, + ?array $paidPlayerWeights, ?array $freeTierWeightsCustom, bool $killModeEnabled, - int $testSafetyLine, - float &$playerProfitTotal, + int $safetyLine, + bool $dbKillEnabled, + float &$poolProfitTotal, array &$resultCounts, array &$tierCounts, array &$buffer, @@ -201,13 +210,21 @@ class WeightTestRunner $lotteryType = $isPaid ? 0 : 1; if ($isPaid) { - $useKillForPaid = $killModeEnabled && $playerProfitTotal >= $testSafetyLine && $killPoolConfig !== null; + $useKillForPaid = $killModeEnabled + && $dbKillEnabled + && $poolProfitTotal >= $safetyLine + && $killPoolConfig !== null; if ($useKillForPaid) { $cfg = $killPoolConfig; $customWeights = null; } else { - $cfg = $paidPoolConfig; - $customWeights = $paidTierWeightsCustom; + // 付费未杀分:模拟玩家档位权重,lottery_config_id 记 default(与 PlayStartLogic 一致) + $cfg = $defaultPoolConfig; + $customWeights = $paidPlayerWeights; + if ($customWeights === null) { + $cfg = $paidPoolConfig; + $customWeights = null; + } } } else { $cfg = $freePoolConfig; @@ -217,7 +234,8 @@ class WeightTestRunner $row = $playLogic->simulateOnePlay($cfg, $dir, $lotteryType, $playAnte, $customWeights, $deptId); $winCoin = (float) ($row['win_coin'] ?? 0); $paidAmount = (float) ($row['paid_amount'] ?? 0); - $playerProfitTotal += $winCoin - $paidAmount; + $perPlayProfit = $isPaid ? ($winCoin - $paidAmount) : $winCoin; + $poolProfitTotal += round($perPlayProfit, 2); $this->aggregate($row, $resultCounts, $tierCounts); $buffer[] = $this->rowForInsert($row, $recordId, $deptId); $done++; @@ -232,6 +250,23 @@ class WeightTestRunner } } + /** + * 从 tier_weights_snapshot 读取付费/免费档位权重快照 + */ + private function resolveTierWeightsSnapshot(DiceRewardConfigRecord $record, string $side): ?array + { + $snap = $record->tier_weights_snapshot ?? null; + if (! is_array($snap)) { + return null; + } + $weights = $snap[$side] ?? null; + if (! is_array($weights) || $weights === []) { + return null; + } + + return $weights; + } + /** * 解析本次测试渠道:优先读库字段,避免 ORM 字段缓存未含 dept_id 时读不到 */