1.优化抽奖券的抽奖逻辑

This commit is contained in:
2026-06-03 17:23:13 +08:00
parent 51105dd1e0
commit 307a942b8e
9 changed files with 136 additions and 60 deletions

View File

@@ -8,6 +8,7 @@
"poolType": "Pool Type", "poolType": "Pool Type",
"placeholderPoolType": "Please select pool type", "placeholderPoolType": "Please select pool type",
"poolTypeNormal": "Normal", "poolTypeNormal": "Normal",
"poolTypeFree": "Free",
"poolTypeKill": "Kill", "poolTypeKill": "Kill",
"poolTypeT1": "T1 High", "poolTypeT1": "T1 High",
"safetyLine": "Safety Line", "safetyLine": "Safety Line",
@@ -25,7 +26,7 @@
"realtime": "Live", "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.", "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", "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", "enableKillScore": "Enable kill score",
"killScoreWeights": "Kill weights (killScore)", "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.", "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", "placeholderName": "Please enter name",
"placeholderPoolType": "Please select pool type", "placeholderPoolType": "Please select pool type",
"poolTypeNormal": "Normal", "poolTypeNormal": "Normal",
"poolTypeFree": "Free",
"poolTypeKill": "Force Kill", "poolTypeKill": "Force Kill",
"poolTypeT1": "T1 High Rate" "poolTypeT1": "T1 High Rate"
}, },

View File

@@ -54,11 +54,14 @@
"weightTest": { "weightTest": {
"title": "One-Click Weight Test", "title": "One-Click Weight Test",
"alertTitle": "Bonus pool logic", "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).", "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", "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", "sectionPaid": "Paid draws",
"sectionFreeAfterPlayAgain": "Free draw tier odds (after play-again)", "sectionFreeAfterPlayAgain": "Free draw tier odds (after play-again)",
"tierProbHintFreeChain": "When using custom tier odds: T1T5 below apply when a free draw runs (tier roll; combined with dice_reward row weights).", "tierProbHintFreeChain": "When using custom tier odds: T1T5 below apply when a free draw runs (tier roll; combined with dice_reward row weights).",
@@ -69,7 +72,7 @@
"labelAnte": "Ante", "labelAnte": "Ante",
"placeholderAnte": "Select ante config", "placeholderAnte": "Select ante config",
"placeholderPaidPool": "Leave empty for custom tier odds below (default: default)", "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 (T1T5), each 0100%, sum of five must not exceed 100%", "tierProbHint": "Custom tier odds (T1T5), each 0100%, sum of five must not exceed 100%",
"tierFieldLabel": "Tier {tier} (%)", "tierFieldLabel": "Tier {tier} (%)",
"tierSumError": "Current sum of five tiers is {sum}%, cannot exceed 100%", "tierSumError": "Current sum of five tiers is {sum}%, cannot exceed 100%",

View File

@@ -8,6 +8,7 @@
"poolType": "奖池类型", "poolType": "奖池类型",
"placeholderPoolType": "请选择奖池类型", "placeholderPoolType": "请选择奖池类型",
"poolTypeNormal": "正常", "poolTypeNormal": "正常",
"poolTypeFree": "免费",
"poolTypeKill": "强制杀猪", "poolTypeKill": "强制杀猪",
"poolTypeT1": "T1高倍率", "poolTypeT1": "T1高倍率",
"safetyLine": "安全线", "safetyLine": "安全线",
@@ -25,7 +26,7 @@
"realtime": "实时", "realtime": "实时",
"profitCalcHint": "累计在 name=default正常奖池上付费每局 += win_coin paid_amountante×1免费每局 += win_coin。用于与安全线比较判定付费抽奖是否切换杀分。弹窗打开期间每 2 秒自动刷新。", "profitCalcHint": "累计在 name=default正常奖池上付费每局 += win_coin paid_amountante×1免费每局 += win_coin。用于与安全线比较判定付费抽奖是否切换杀分。弹窗打开期间每 2 秒自动刷新。",
"tierRuleTitle": "付费抽奖档位规则", "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": "开启杀分", "enableKillScore": "开启杀分",
"killScoreWeights": "杀分权重killScore", "killScoreWeights": "杀分权重killScore",
"killWeightNote": "杀分权重请在列表中编辑 name=killScore强制杀猪记录本弹窗仅配置 default 奖池的安全线与杀分开关。", "killWeightNote": "杀分权重请在列表中编辑 name=killScore强制杀猪记录本弹窗仅配置 default 奖池的安全线与杀分开关。",
@@ -57,6 +58,7 @@
"placeholderName": "请输入名称", "placeholderName": "请输入名称",
"placeholderPoolType": "请选择奖池类型", "placeholderPoolType": "请选择奖池类型",
"poolTypeNormal": "正常", "poolTypeNormal": "正常",
"poolTypeFree": "免费",
"poolTypeKill": "强制杀猪", "poolTypeKill": "强制杀猪",
"poolTypeT1": "T1高倍率" "poolTypeT1": "T1高倍率"
}, },

View File

@@ -54,11 +54,14 @@
"weightTest": { "weightTest": {
"title": "一键测试权重", "title": "一键测试权重",
"alertTitle": "彩金池逻辑说明", "alertTitle": "彩金池逻辑说明",
"alertBody": "测试模式默认不启用杀分切换;可通过下方“杀分开关 + 安全线”在测试内启用:当模拟玩家累计盈利达到安全线后,付费抽奖切换到 killScore。", "alertBody": "测试模式默认不启用杀分切换;开启「测试内杀分」后,判定规则与线上一致:以 default 彩金池累计盈利profit_amount对比安全线且需 kill_enabled=开启、存在 killScore 配置。",
"chainModeHint": "模拟方式:只配置付费抽奖次数(顺/逆时针)。付费抽到「再来一次」或 T5 时,下一局自动为免费抽奖,底注与触发局相同,抽奖类型记为免费、付费金额记为 0。免费抽奖的档位概率由下方「免费抽奖」配置决定含通过再来一次触发的后续免费局。", "chainModeHint": "模拟方式:只配置付费抽奖次数(顺/逆时针)。付费抽到「再来一次」或 T5 时,下一局自动为免费抽奖,底注与触发局相同,抽奖类型记为免费、付费金额记为 0。免费抽奖的档位概率由下方「免费抽奖」配置决定含通过再来一次触发的后续免费局。",
"killModeHint": "杀分开关开启后:以“模拟玩家累计盈利”作为判定值;当累计盈利 >= 安全线时,后续付费抽奖 killScore 配置抽取;免费抽奖仍按“免费抽奖配置”执行。", "killModeHint": "杀分开关开启后:从 default 奖池当前 profit_amount 起步,测试内逐局累加(付费=win_coin-paid_amount免费=win_coin;当累计盈利 安全线且 kill_enabled 开启时,后续付费抽奖 killScore免费抽奖仍走 name=free 奖池。",
"labelKillModeEnabled": "开启测试内杀分", "labelKillModeEnabled": "开启测试内杀分",
"labelTestSafetyLine": "测试安全线", "labelTestSafetyLine": "default 安全线",
"killEnabledOn": "杀分已开启",
"killEnabledOff": "杀分已关闭",
"poolProfitPrefix": "当前池盈利 ",
"sectionPaid": "付费抽奖", "sectionPaid": "付费抽奖",
"sectionFreeAfterPlayAgain": "免费抽奖(再来一次后的档位概率)", "sectionFreeAfterPlayAgain": "免费抽奖(再来一次后的档位概率)",
"tierProbHintFreeChain": "当使用自定义档位时:以下为「免费抽奖」时 T1T5 的档位概率(仅在有免费局时参与摇档,与 dice_reward 格子权重共同决定结果)。", "tierProbHintFreeChain": "当使用自定义档位时:以下为「免费抽奖」时 T1T5 的档位概率(仅在有免费局时参与摇档,与 dice_reward 格子权重共同决定结果)。",
@@ -69,7 +72,7 @@
"labelAnte": "底注", "labelAnte": "底注",
"placeholderAnte": "请选择底注配置", "placeholderAnte": "请选择底注配置",
"placeholderPaidPool": "不选则下方自定义档位概率(默认 default", "placeholderPaidPool": "不选则下方自定义档位概率(默认 default",
"placeholderFreePool": "不选则下方自定义档位概率(默认 killScore", "placeholderFreePool": "不选则下方自定义档位概率(默认 free 免费奖池",
"tierProbHint": "自定义档位概率T1T5每档 0-100%,五档之和不能超过 100%", "tierProbHint": "自定义档位概率T1T5每档 0-100%,五档之和不能超过 100%",
"tierFieldLabel": "档位 {tier}%", "tierFieldLabel": "档位 {tier}%",
"tierSumError": "当前五档之和为 {sum}%,不能超过 100%", "tierSumError": "当前五档之和为 {sum}%,不能超过 100%",

View File

@@ -91,6 +91,7 @@
const typeFormatter = (row: Record<string, unknown>) => { const typeFormatter = (row: Record<string, unknown>) => {
const n = String(row.name ?? '') const n = String(row.name ?? '')
if (n === 'default') return t('page.search.poolTypeNormal') 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 === 'killScore') return t('page.search.poolTypeKill')
if (n === 'up') return t('page.search.poolTypeT1') if (n === 'up') return t('page.search.poolTypeT1')
return n || '-' return n || '-'

View File

@@ -44,14 +44,11 @@
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
<ElCol :span="8"> <ElCol :span="8">
<ElFormItem :label="$t('page.weightTest.labelTestSafetyLine')" prop="test_safety_line"> <ElFormItem :label="$t('page.weightTest.labelTestSafetyLine')">
<ElInputNumber <ElInput
v-model="form.test_safety_line" :model-value="defaultPoolSafetyLineText"
:min="0" readonly
:step="100"
:disabled="!form.kill_mode_enabled" :disabled="!form.kill_mode_enabled"
controls-position="right"
style="width: 100%"
/> />
</ElFormItem> </ElFormItem>
</ElCol> </ElCol>
@@ -221,17 +218,27 @@
free_tier_weights: { T1: 20, T2: 20, T3: 20, T4: 20, T5: 20 } as Record<string, number>, free_tier_weights: { T1: 20, T2: 20, T3: 20, T4: 20, T5: 20 } as Record<string, number>,
paid_s_count: 100, paid_s_count: 100,
paid_n_count: 100, paid_n_count: 100,
kill_mode_enabled: false, kill_mode_enabled: false
test_safety_line: 5000 })
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<Array<{ id: number; name: string }>>([]) const lotteryOptions = ref<Array<{ id: number; name: string }>>([])
const paidLotteryOptions = computed(() => const paidLotteryOptions = computed(() =>
lotteryOptions.value.filter((r) => r.name === 'default') lotteryOptions.value.filter((r) => r.name === 'default')
) )
const freeLotteryOptions = computed(() => { const freeLotteryOptions = computed(() => {
const list = lotteryOptions.value.filter((r) => r.name === 'killScore') const list = lotteryOptions.value.filter((r) => r.name === 'free')
return list.length > 0 ? list : lotteryOptions.value 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) const running = ref(false)
function onClose() { 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() { async function loadLotteryOptions() {
try { try {
const list = await lotteryPoolApi.getOptions(resolveDeptParams()) const list = await lotteryPoolApi.getOptions(resolveDeptParams())
@@ -332,9 +352,9 @@
if (normal) { if (normal) {
form.paid_lottery_config_id = normal.id form.paid_lottery_config_id = normal.id
} }
const kill = list.find((r: { name?: string }) => r.name === 'killScore') const freePool = list.find((r: { name?: string }) => r.name === 'free')
if (kill) { if (freePool) {
form.free_lottery_config_id = kill.id form.free_lottery_config_id = freePool.id
} else if (list.length > 0) { } else if (list.length > 0) {
form.free_lottery_config_id = list[0].id form.free_lottery_config_id = list[0].id
} }
@@ -353,7 +373,7 @@
free_n_count: 0, free_n_count: 0,
chain_free_mode: true, chain_free_mode: true,
kill_mode_enabled: form.kill_mode_enabled, 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) { if (form.paid_lottery_config_id != null) {
payload.paid_lottery_config_id = form.paid_lottery_config_id payload.paid_lottery_config_id = form.paid_lottery_config_id
@@ -382,10 +402,6 @@
ElMessage.warning(t('page.weightTest.warnPaidSpins')) ElMessage.warning(t('page.weightTest.warnPaidSpins'))
return false 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 needPaidTier = form.paid_lottery_config_id == null
const needFreeTier = form.free_lottery_config_id == null const needFreeTier = form.free_lottery_config_id == null
if (needPaidTier) { if (needPaidTier) {
@@ -437,6 +453,7 @@
if (v) { if (v) {
void loadAnteOptions() void loadAnteOptions()
void loadLotteryOptions() void loadLotteryOptions()
void loadDefaultPoolInfo()
} else { } else {
onClose() onClose()
} }
@@ -448,6 +465,7 @@
if (visible.value) { if (visible.value) {
void loadAnteOptions() void loadAnteOptions()
void loadLotteryOptions() void loadLotteryOptions()
void loadDefaultPoolInfo()
} }
} }
) )

View File

@@ -118,7 +118,8 @@ class PlayStartLogic
} }
$configType0 = DiceLotteryPoolConfig::where('name', 'default')->where('dept_id', $configDeptId)->find(); $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) { if (!$configType0) {
throw new ApiException('Lottery pool config not found (name=default required)'); 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_amountdefault 奖池)
$poolProfitTotal = $configType0->profit_amount ?? 0; $poolProfitTotal = $configType0->profit_amount ?? 0;
$safetyLine = (int) ($configType0->safety_line ?? 0); $safetyLine = (int) ($configType0->safety_line ?? 0);
$killEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1; $killEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1;
// 盈利>=安全线且开启杀分:付费/免费都用 killScore盈利<安全线:付费用玩家权重,免费用 killScore无则用 default
// 记录 lottery_config_id用池权重时记对应池付费用玩家权重时记 default $usePaidKill = $ticketType === self::LOTTERY_TYPE_PAID
$usePoolWeights = ($ticketType === self::LOTTERY_TYPE_PAID && $killEnabled && $poolProfitTotal >= $safetyLine && $configType1 !== null) && $killEnabled
|| ($ticketType === self::LOTTERY_TYPE_FREE); && $poolProfitTotal >= $safetyLine
$config = $usePoolWeights && $configKill !== null;
? (($ticketType === self::LOTTERY_TYPE_FREE && $configType1 === null) ? $configType0 : $configType1)
: $configType0; 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 // 按档位 T1-T5 抽取后,从 DiceReward 表按当前方向取该档位数据,再按 weight 抽取一条得到 grid_number
$rewardInstance = DiceReward::getCachedInstance($configDeptId); $rewardInstance = DiceReward::getCachedInstance($configDeptId);

View File

@@ -96,7 +96,7 @@ class DiceRewardController extends BaseController
* 参数lottery_config_id 可选paid_tier_weights / free_tier_weights 自定义档位; * 参数lottery_config_id 可选paid_tier_weights / free_tier_weights 自定义档位;
* paid_s_count, paid_n_count * paid_s_count, paid_n_count
* chain_free_mode=1仅按付费次数模拟付费抽到再来一次/T5 则在队列中插入免费局同底注、lottery_type=免费、paid_amount=0 * 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')] #[Permission('一键测试权重', 'dice:reward:index:startWeightTest')]
public function startWeightTest(Request $request): Response public function startWeightTest(Request $request): Response

View File

@@ -15,7 +15,10 @@ use support\think\Db;
/** /**
* 一键测试权重:单进程后台执行模拟摇色子,写入 dice_play_record_test 并更新 dice_reward_config_record 进度 * 一键测试权重:单进程后台执行模拟摇色子,写入 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 class WeightTestRunner
{ {
@@ -72,7 +75,8 @@ class WeightTestRunner
DiceRewardConfig::clearRequestInstance(); DiceRewardConfig::clearRequestInstance();
$configType0 = DiceLotteryPoolConfig::findByNameForDept('default', $deptId); $configType0 = DiceLotteryPoolConfig::findByNameForDept('default', $deptId);
$configType1 = DiceLotteryPoolConfig::findByNameForDept('killScore', $deptId); $configKill = DiceLotteryPoolConfig::findByNameForDept('killScore', $deptId);
$configFree = DiceLotteryPoolConfig::findByNameForDept('free', $deptId);
if (!$configType0) { if (!$configType0) {
$this->markFailed($recordId, '彩金池配置 name=default 不存在(当前渠道)'); $this->markFailed($recordId, '彩金池配置 name=default 不存在(当前渠道)');
return; return;
@@ -92,9 +96,9 @@ class WeightTestRunner
if (!$paidPoolConfig || AdminScopeHelper::normalizeRecordDeptId($paidPoolConfig->dept_id ?? null) !== $deptId) { if (!$paidPoolConfig || AdminScopeHelper::normalizeRecordDeptId($paidPoolConfig->dept_id ?? null) !== $deptId) {
$paidPoolConfig = $configType0; $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) { if (!$freePoolConfig || AdminScopeHelper::normalizeRecordDeptId($freePoolConfig->dept_id ?? null) !== $deptId) {
$freePoolConfig = $configType1 ?: $configType0; $freePoolConfig = $configFree ?: $configType0;
} }
if ($paidTierWeightsCustom !== null && array_sum($paidTierWeightsCustom) <= 0) { if ($paidTierWeightsCustom !== null && array_sum($paidTierWeightsCustom) <= 0) {
@@ -111,13 +115,14 @@ class WeightTestRunner
DiceReward::clearRequestInstance(); DiceReward::clearRequestInstance();
$killModeEnabled = (int) ($record->kill_mode_enabled ?? 0) === 1; $killModeEnabled = (int) ($record->kill_mode_enabled ?? 0) === 1;
$testSafetyLine = (int) ($record->test_safety_line ?? 5000); $safetyLine = (int) ($configType0->safety_line ?? 0);
if ($testSafetyLine < 0) { $dbKillEnabled = ((int) ($configType0->kill_enabled ?? 1)) === 1;
$testSafetyLine = 0;
}
// 测试内“玩家累计盈利”:用于控制付费局是否切换杀分 // 彩金池累计盈利:与线上一致,从 default.profit_amount 起步并在测试内逐局累加
$playerProfitTotal = 0.0; $poolProfitTotal = (float) ($configType0->profit_amount ?? 0);
// 付费未杀分时的模拟玩家档位权重(自定义 > 快照 > 兜底奖池)
$paidPlayerWeights = $paidTierWeightsCustom ?? $this->resolveTierWeightsSnapshot($record, 'paid');
$playLogic = new PlayStartLogic(); $playLogic = new PlayStartLogic();
$resultCounts = []; $resultCounts = [];
@@ -133,14 +138,16 @@ class WeightTestRunner
$paidS, $paidS,
$paidN, $paidN,
$ante, $ante,
$configType0,
$paidPoolConfig, $paidPoolConfig,
$freePoolConfig, $freePoolConfig,
$configType1, $configKill,
$paidTierWeightsCustom, $paidPlayerWeights,
$freeTierWeightsCustom, $freeTierWeightsCustom,
$killModeEnabled, $killModeEnabled,
$testSafetyLine, $safetyLine,
$playerProfitTotal, $dbKillEnabled,
$poolProfitTotal,
$resultCounts, $resultCounts,
$tierCounts, $tierCounts,
$buffer, $buffer,
@@ -172,14 +179,16 @@ class WeightTestRunner
int $paidS, int $paidS,
int $paidN, int $paidN,
int $ante, int $ante,
$defaultPoolConfig,
$paidPoolConfig, $paidPoolConfig,
$freePoolConfig, $freePoolConfig,
$killPoolConfig, $killPoolConfig,
?array $paidTierWeightsCustom, ?array $paidPlayerWeights,
?array $freeTierWeightsCustom, ?array $freeTierWeightsCustom,
bool $killModeEnabled, bool $killModeEnabled,
int $testSafetyLine, int $safetyLine,
float &$playerProfitTotal, bool $dbKillEnabled,
float &$poolProfitTotal,
array &$resultCounts, array &$resultCounts,
array &$tierCounts, array &$tierCounts,
array &$buffer, array &$buffer,
@@ -201,13 +210,21 @@ class WeightTestRunner
$lotteryType = $isPaid ? 0 : 1; $lotteryType = $isPaid ? 0 : 1;
if ($isPaid) { if ($isPaid) {
$useKillForPaid = $killModeEnabled && $playerProfitTotal >= $testSafetyLine && $killPoolConfig !== null; $useKillForPaid = $killModeEnabled
&& $dbKillEnabled
&& $poolProfitTotal >= $safetyLine
&& $killPoolConfig !== null;
if ($useKillForPaid) { if ($useKillForPaid) {
$cfg = $killPoolConfig; $cfg = $killPoolConfig;
$customWeights = null; $customWeights = null;
} else { } else {
$cfg = $paidPoolConfig; // 付费未杀分模拟玩家档位权重lottery_config_id 记 default与 PlayStartLogic 一致)
$customWeights = $paidTierWeightsCustom; $cfg = $defaultPoolConfig;
$customWeights = $paidPlayerWeights;
if ($customWeights === null) {
$cfg = $paidPoolConfig;
$customWeights = null;
}
} }
} else { } else {
$cfg = $freePoolConfig; $cfg = $freePoolConfig;
@@ -217,7 +234,8 @@ class WeightTestRunner
$row = $playLogic->simulateOnePlay($cfg, $dir, $lotteryType, $playAnte, $customWeights, $deptId); $row = $playLogic->simulateOnePlay($cfg, $dir, $lotteryType, $playAnte, $customWeights, $deptId);
$winCoin = (float) ($row['win_coin'] ?? 0); $winCoin = (float) ($row['win_coin'] ?? 0);
$paidAmount = (float) ($row['paid_amount'] ?? 0); $paidAmount = (float) ($row['paid_amount'] ?? 0);
$playerProfitTotal += $winCoin - $paidAmount; $perPlayProfit = $isPaid ? ($winCoin - $paidAmount) : $winCoin;
$poolProfitTotal += round($perPlayProfit, 2);
$this->aggregate($row, $resultCounts, $tierCounts); $this->aggregate($row, $resultCounts, $tierCounts);
$buffer[] = $this->rowForInsert($row, $recordId, $deptId); $buffer[] = $this->rowForInsert($row, $recordId, $deptId);
$done++; $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 时读不到 * 解析本次测试渠道:优先读库字段,避免 ORM 字段缓存未含 dept_id 时读不到
*/ */