重新设置抽奖底注金额为1,优化页面样式

This commit is contained in:
2026-03-31 17:23:16 +08:00
parent d54a9c9281
commit 6ed34b97df
24 changed files with 118 additions and 94 deletions

View File

@@ -23,7 +23,7 @@
"poolName": "Pool Name", "poolName": "Pool Name",
"playerProfit": "Player Total Profit (profit_amount):", "playerProfit": "Player Total Profit (profit_amount):",
"realtime": "Live", "realtime": "Live",
"profitCalcHint": "Profit per round: paid = win_coin (incl. BIGWIN) - paid_amount (= ante×100); free = win_coin. Refreshes every 2s while open.", "profitCalcHint": "Profit per round: paid = win_coin (incl. BIGWIN) - paid_amount (= ante×1); free = win_coin. Refreshes every 2s while open.",
"tierRuleTitle": "Tier Rule", "tierRuleTitle": "Tier Rule",
"tierRuleContent": "When player profit in this pool is below safety line, use player T*_weight; when above or equal, use pool T*_weight (kill).", "tierRuleContent": "When player profit in this pool is below safety line, use player T*_weight; when above or equal, use pool T*_weight (kill).",
"killScoreWeights": "Kill weights", "killScoreWeights": "Kill weights",

View File

@@ -23,7 +23,7 @@
"poolName": "池子名称", "poolName": "池子名称",
"playerProfit": "玩家累计盈利profit_amount", "playerProfit": "玩家累计盈利profit_amount",
"realtime": "实时", "realtime": "实时",
"profitCalcHint": "计算方式:付费每局按“赢取平台币 win_coin含 BIGWIN减去付费金额 压注金额paid_amount= 压注倍数ante×100”累加免费每局按“玩家赢得平台币win_coin”累加。弹窗打开期间每 2 秒自动刷新", "profitCalcHint": "计算方式:付费每局按“赢取平台币 win_coin含 BIGWIN减去付费金额 压注金额paid_amount= 压注倍数ante×1”累加免费每局按“玩家赢得平台币win_coin”累加。弹窗打开期间每 2 秒自动刷新",
"tierRuleTitle": "抽奖档位规则", "tierRuleTitle": "抽奖档位规则",
"tierRuleContent": "当玩家在当前彩金池的累计盈利 低于安全线 时,按 玩家 的 T*_weight 权重抽取档位;当累计盈利 高于或等于安全线 时,按 当前彩金池 的 T*_weight 权重抽取档位(杀分)。", "tierRuleContent": "当玩家在当前彩金池的累计盈利 低于安全线 时,按 玩家 的 T*_weight 权重抽取档位;当累计盈利 高于或等于安全线 时,按 当前彩金池 的 T*_weight 权重抽取档位(杀分)。",
"killScoreWeights": "杀分权重", "killScoreWeights": "杀分权重",

View File

@@ -6,7 +6,11 @@
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData"> <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left> <template #left>
<ElSpace wrap> <ElSpace wrap>
<ElButton v-permission="'dice:ante_config:index:save'" @click="showDialog('add')" v-ripple> <ElButton
v-permission="'dice:ante_config:index:save'"
@click="showDialog('add')"
v-ripple
>
<template #icon> <template #icon>
<ArtSvgIcon icon="ri:add-fill" /> <ArtSvgIcon icon="ri:add-fill" />
</template> </template>
@@ -40,7 +44,7 @@
@pagination:current-change="handleCurrentChange" @pagination:current-change="handleCurrentChange"
> >
<template #is_default="{ row }"> <template #is_default="{ row }">
<ElTag :type="row.is_default === 1 ? 'success' : 'info'" size="small"> <ElTag :type="row.is_default === 1 ? 'primary' : 'warning'" size="small">
{{ row.is_default === 1 ? $t('page.table.defaultYes') : $t('page.table.defaultNo') }} {{ row.is_default === 1 ? $t('page.table.defaultYes') : $t('page.table.defaultNo') }}
</ElTag> </ElTag>
</template> </template>
@@ -111,7 +115,13 @@
{ prop: 'name', label: 'page.table.name', align: 'center' }, { prop: 'name', label: 'page.table.name', align: 'center' },
{ prop: 'title', label: 'page.table.title', align: 'center' }, { prop: 'title', label: 'page.table.title', align: 'center' },
{ prop: 'mult', label: 'page.table.mult', align: 'center' }, { prop: 'mult', label: 'page.table.mult', align: 'center' },
{ prop: 'is_default', label: 'page.table.isDefault', width: 110, align: 'center', useSlot: true }, {
prop: 'is_default',
label: 'page.table.isDefault',
width: 110,
align: 'center',
useSlot: true
},
{ prop: 'create_time', label: 'page.table.createTime', width: 170, align: 'center' }, { prop: 'create_time', label: 'page.table.createTime', width: 170, align: 'center' },
{ prop: 'update_time', label: 'page.table.updateTime', width: 170, align: 'center' }, { prop: 'update_time', label: 'page.table.updateTime', width: 170, align: 'center' },
{ {

View File

@@ -99,6 +99,9 @@
<template #reward_win_coin="{ row }"> <template #reward_win_coin="{ row }">
<span>{{ formatPlatformCoin(row?.reward_win_coin) }}</span> <span>{{ formatPlatformCoin(row?.reward_win_coin) }}</span>
</template> </template>
<template #paid_amount="{ row }">
<span>{{ formatPlatformCoin(row?.paid_amount) }}</span>
</template>
<!-- 摇取点数 tag --> <!-- 摇取点数 tag -->
<template #roll_array="{ row }"> <template #roll_array="{ row }">
<ElTag size="small"> <ElTag size="small">
@@ -157,7 +160,7 @@
direction: undefined direction: undefined
}) })
/** 当前筛选下平台总盈利(付费抽奖次数×100 - 玩家总收益) */ /** 当前筛选下平台总盈利(付费金额 paid_amount 求和 - 玩家总收益) */
const totalWinCoin = ref<number | null>(null) const totalWinCoin = ref<number | null>(null)
const listApi = async (params: Record<string, any>) => { const listApi = async (params: Record<string, any>) => {
@@ -197,7 +200,7 @@
if (val === '' || val === null || val === undefined) return '-' if (val === '' || val === null || val === undefined) return '-'
const n = typeof val === 'number' ? val : Number(val) const n = typeof val === 'number' ? val : Number(val)
if (!Number.isFinite(n)) return '-' if (!Number.isFinite(n)) return '-'
return String(Math.trunc(n)) return n.toFixed(2)
} }
// 表格配置 // 表格配置
@@ -235,7 +238,7 @@
{ prop: 'lottery_type', label: 'page.table.drawType', width: 100, useSlot: true }, { prop: 'lottery_type', label: 'page.table.drawType', width: 100, useSlot: true },
{ prop: 'direction', label: 'page.table.direction', width: 90, useSlot: true }, { prop: 'direction', label: 'page.table.direction', width: 90, useSlot: true },
{ prop: 'ante', label: 'page.table.ante', width: 80, align: 'center' }, { prop: 'ante', label: 'page.table.ante', width: 80, align: 'center' },
{ prop: 'paid_amount', label: 'page.table.paidAmount', width: 110, align: 'center' }, { prop: 'paid_amount', label: 'page.table.paidAmount', width: 110, align: 'center', useSlot: true },
{ prop: 'is_win', label: 'page.table.isBigWin', width: 100, useSlot: true }, { prop: 'is_win', label: 'page.table.isBigWin', width: 100, useSlot: true },
{ prop: 'win_coin', label: 'page.table.winCoin', width: 110, useSlot: true }, { prop: 'win_coin', label: 'page.table.winCoin', width: 110, useSlot: true },
{ prop: 'super_win_coin', label: 'page.table.superWinCoin', width: 120, useSlot: true }, { prop: 'super_win_coin', label: 'page.table.superWinCoin', width: 120, useSlot: true },

View File

@@ -171,7 +171,7 @@
roll_number: undefined roll_number: undefined
}) })
// 当前筛选下平台总盈利(付费抽奖次数×100 - 玩家总收益) // 当前筛选下平台总盈利(付费金额 paid_amount 求和 - 玩家总收益)
const totalWinCoin = ref<number | null>(null) const totalWinCoin = ref<number | null>(null)
const listApi = async (params: Record<string, any>) => { const listApi = async (params: Record<string, any>) => {
@@ -203,7 +203,7 @@
if (val === '' || val === null || val === undefined) return '-' if (val === '' || val === null || val === undefined) return '-'
const n = typeof val === 'number' ? val : Number(val) const n = typeof val === 'number' ? val : Number(val)
if (!Number.isFinite(n)) return '-' if (!Number.isFinite(n)) return '-'
return String(Math.trunc(n)) return n.toFixed(2)
} }
const handleClearAll = async () => { const handleClearAll = async () => {

View File

@@ -167,7 +167,7 @@
{ {
prop: 'coin', prop: 'coin',
label: 'page.table.coin', label: 'page.table.coin',
width: 100, width: 110,
align: 'center', align: 'center',
useSlot: true useSlot: true
}, },

View File

@@ -146,11 +146,11 @@
return player?.username ?? row.player_id ?? '-' return player?.username ?? row.player_id ?? '-'
} }
function formatInteger(val: unknown): string { function formatMoney2(val: unknown): string {
if (val === '' || val === null || val === undefined) return '-' if (val === '' || val === null || val === undefined) return '-'
const n = typeof val === 'number' ? val : Number(val) const n = typeof val === 'number' ? val : Number(val)
if (!Number.isFinite(n)) return '-' if (!Number.isFinite(n)) return '-'
return String(Math.trunc(n)) return n.toFixed(2)
} }
// 表格配置 // 表格配置
@@ -202,14 +202,14 @@
label: 'page.table.walletBefore', label: 'page.table.walletBefore',
width: 110, width: 110,
align: 'center', align: 'center',
formatter: (row: Record<string, any>) => formatInteger(row?.wallet_before) formatter: (row: Record<string, any>) => formatMoney2(row?.wallet_before)
}, },
{ {
prop: 'wallet_after', prop: 'wallet_after',
label: 'page.table.walletAfter', label: 'page.table.walletAfter',
width: 110, width: 110,
align: 'center', align: 'center',
formatter: (row: Record<string, any>) => formatInteger(row?.wallet_after) formatter: (row: Record<string, any>) => formatMoney2(row?.wallet_after)
}, },
{ {
prop: 'remark', prop: 'remark',

View File

@@ -45,7 +45,8 @@
<el-input-number <el-input-number
v-model="formData.coin" v-model="formData.coin"
:placeholder="$t('page.form.placeholderCoinChange')" :placeholder="$t('page.form.placeholderCoinChange')"
:precision="0" :precision="2"
:step="1"
style="width: 100%" style="width: 100%"
@change="onCoinChange" @change="onCoinChange"
:disabled="dialogType === 'edit'" :disabled="dialogType === 'edit'"
@@ -55,7 +56,7 @@
<el-input-number <el-input-number
v-model="formData.wallet_before" v-model="formData.wallet_before"
:placeholder="$t('page.form.placeholderWalletBefore')" :placeholder="$t('page.form.placeholderWalletBefore')"
:precision="0" :precision="2"
disabled disabled
style="width: 100%" style="width: 100%"
/> />
@@ -64,7 +65,7 @@
<el-input-number <el-input-number
v-model="formData.wallet_after" v-model="formData.wallet_after"
:placeholder="$t('page.form.placeholderWalletAfter')" :placeholder="$t('page.form.placeholderWalletAfter')"
:precision="0" :precision="2"
disabled disabled
style="width: 100%" style="width: 100%"
/> />
@@ -178,7 +179,7 @@
function calcWalletAfter() { function calcWalletAfter() {
const before = Number(formData.wallet_before) || 0 const before = Number(formData.wallet_before) || 0
const coin = Number(formData.coin) || 0 const coin = Number(formData.coin) || 0
formData.wallet_after = Math.trunc(before + coin) formData.wallet_after = Number((before + coin).toFixed(2))
} }
watch( watch(
@@ -204,11 +205,11 @@
} }
} }
function normalizeInteger(val: unknown, fallback: number): number { function normalizeMoney2(val: unknown, fallback: number): number {
if (val === '' || val === null || val === undefined) return fallback if (val === '' || val === null || val === undefined) return fallback
const n = typeof val === 'number' ? val : Number(val) const n = typeof val === 'number' ? val : Number(val)
if (!Number.isFinite(n)) return fallback if (!Number.isFinite(n)) return fallback
return Math.trunc(n) return Number(n.toFixed(2))
} }
const initForm = () => { const initForm = () => {
@@ -218,9 +219,9 @@
formData.player_id = formData.player_id =
props.data.player_id != null && props.data.player_id !== '' ? Number(props.data.player_id) : null props.data.player_id != null && props.data.player_id !== '' ? Number(props.data.player_id) : null
formData.type = props.data.type != null && props.data.type !== '' ? Number(props.data.type) : null formData.type = props.data.type != null && props.data.type !== '' ? Number(props.data.type) : null
formData.coin = normalizeInteger(props.data.coin, 0) formData.coin = normalizeMoney2(props.data.coin, 0)
formData.wallet_before = normalizeInteger(props.data.wallet_before, 0) formData.wallet_before = normalizeMoney2(props.data.wallet_before, 0)
formData.wallet_after = normalizeInteger(props.data.wallet_after, 0) formData.wallet_after = normalizeMoney2(props.data.wallet_after, 0)
formData.remark = props.data.remark ?? '' formData.remark = props.data.remark ?? ''
} }

View File

@@ -70,11 +70,11 @@
return api.list({ ...params, direction: currentDirection.value }) return api.list({ ...params, direction: currentDirection.value })
} }
function formatInteger(val: unknown): string { function formatMoney2(val: unknown): string {
if (val === '' || val === null || val === undefined) return '-' if (val === '' || val === null || val === undefined) return '-'
const n = typeof val === 'number' ? val : Number(val) const n = typeof val === 'number' ? val : Number(val)
if (!Number.isFinite(n)) return '-' if (!Number.isFinite(n)) return '-'
return String(Math.trunc(n)) return n.toFixed(2)
} }
const handleSearch = (params: Record<string, any>) => { const handleSearch = (params: Record<string, any>) => {
@@ -129,7 +129,7 @@
label: 'page.table.realEv', label: 'page.table.realEv',
width: 110, width: 110,
align: 'center', align: 'center',
formatter: (row: Record<string, any>) => formatInteger(row?.real_ev) formatter: (row: Record<string, any>) => formatMoney2(row?.real_ev)
}, },
{ prop: 'remark', label: 'page.table.remark', minWidth: 80, align: 'center', showOverflowTooltip: true }, { prop: 'remark', label: 'page.table.remark', minWidth: 80, align: 'center', showOverflowTooltip: true },
{ prop: 'weight', label: 'page.table.weight', width: 110, align: 'center' } { prop: 'weight', label: 'page.table.weight', width: 110, align: 'center' }

View File

@@ -62,7 +62,7 @@
show-overflow-tooltip show-overflow-tooltip
> >
<template #default="{ row }"> <template #default="{ row }">
<span>{{ formatInteger(row?.real_ev) }}</span> <span>{{ formatMoney2(row?.real_ev) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@@ -254,11 +254,11 @@
import api from '../../../api/reward/index' import api from '../../../api/reward/index'
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue' import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
function formatInteger(val: unknown): string { function formatMoney2(val: unknown): string {
if (val === '' || val === null || val === undefined) return '-' if (val === '' || val === null || val === undefined) return '-'
const n = typeof val === 'number' ? val : Number(val) const n = typeof val === 'number' ? val : Number(val)
if (!Number.isFinite(n)) return '-' if (!Number.isFinite(n)) return '-'
return String(Math.trunc(n)) return n.toFixed(2)
} }
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'

View File

@@ -56,7 +56,7 @@
show-overflow-tooltip show-overflow-tooltip
> >
<template #default="{ row }"> <template #default="{ row }">
<span>{{ formatInteger(row?.real_ev) }}</span> <span>{{ formatMoney2(row?.real_ev) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@@ -319,11 +319,11 @@
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue' import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
function formatInteger(val: unknown): string { function formatMoney2(val: unknown): string {
if (val === '' || val === null || val === undefined) return '-' if (val === '' || val === null || val === undefined) return '-'
const n = typeof val === 'number' ? val : Number(val) const n = typeof val === 'number' ? val : Number(val)
if (!Number.isFinite(n)) return '-' if (!Number.isFinite(n)) return '-'
return String(Math.trunc(n)) return n.toFixed(2)
} }

View File

@@ -81,13 +81,14 @@
@change="handleRealEvChange(row)" @change="handleRealEvChange(row)"
controls-position="right" controls-position="right"
size="small" size="small"
:step="1"
class="full-width" class="full-width"
/> />
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn :label="$t('page.configPage.colRealReward')" min-width="130" align="center"> <ElTableColumn :label="$t('page.configPage.colRealReward')" min-width="130" align="center">
<template #default="{ row }"> <template #default="{ row }">
<span>{{ calcRealReward(row.real_ev) }}</span> <span>{{ formatMoney2(calcRealReward(row.real_ev)) }}</span>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn :label="$t('page.configPage.colTier')" width="100" align="center"> <ElTableColumn :label="$t('page.configPage.colTier')" width="100" align="center">
@@ -171,6 +172,7 @@
@change="handleRealEvChange(row)" @change="handleRealEvChange(row)"
controls-position="right" controls-position="right"
size="small" size="small"
:step="1"
class="full-width" class="full-width"
/> />
</template> </template>
@@ -501,11 +503,18 @@
typeof row.real_ev === 'number' && !Number.isNaN(row.real_ev) typeof row.real_ev === 'number' && !Number.isNaN(row.real_ev)
? row.real_ev ? row.real_ev
: Number(row.real_ev) : Number(row.real_ev)
const text = Number.isNaN(n) ? '' : String(n) const text = Number.isNaN(n) ? '' : Number(n).toFixed(2)
row.ui_text = text row.ui_text = text
row.ui_text_en = text row.ui_text_en = text
} }
function formatMoney2(val: unknown): string {
if (val === '' || val === null || val === undefined) return '-'
const n = typeof val === 'number' ? val : Number(val)
if (!Number.isFinite(n)) return '-'
return n.toFixed(2)
}
async function handleCreateRewardReference() { async function handleCreateRewardReference() {
try { try {
await ElMessageBox.confirm( await ElMessageBox.confirm(

View File

@@ -278,13 +278,13 @@ function uiTextByTierWhenStandards(
if (tier === 'T5') { if (tier === 'T5') {
return { ui_text: '再来一次', ui_text_en: 'Once again' } return { ui_text: '再来一次', ui_text_en: 'Once again' }
} }
const value = String(realEv) const value = Number.isFinite(realEv) ? realEv.toFixed(2) : String(realEv)
return { ui_text: value, ui_text_en: value } return { ui_text: value, ui_text_en: value }
} }
/** 展示文案:直接使用真实结算值(中英文相同) */ /** 展示文案:直接使用真实结算值(中英文相同) */
function uiTextFromRealEv(realEv: number): { ui_text: string; ui_text_en: string } { function uiTextFromRealEv(realEv: number): { ui_text: string; ui_text_en: string } {
const value = String(realEv) const value = Number.isFinite(realEv) ? realEv.toFixed(2) : String(realEv)
return { ui_text: value, ui_text_en: value } return { ui_text: value, ui_text_en: value }
} }

View File

@@ -149,7 +149,7 @@
if (v === null || v === undefined || v === '') return dash if (v === null || v === undefined || v === '') return dash
const n = Number(v) const n = Number(v)
if (Number.isNaN(n)) return dash if (Number.isNaN(n)) return dash
return String(n) return n.toFixed(2)
} }
/** 链式再来一次1=是新库字段JSON 旧数据用 tier_weights_snapshot.chain_free_mode */ /** 链式再来一次1=是新库字段JSON 旧数据用 tier_weights_snapshot.chain_free_mode */

View File

@@ -74,7 +74,7 @@ class GameController extends BaseController
* 购买抽奖券 * 购买抽奖券
* POST /api/game/buyLotteryTickets * POST /api/game/buyLotteryTickets
* header: token由 TokenMiddleware 注入 request->player_id * header: token由 TokenMiddleware 注入 request->player_id
* body: count = 1 | 5 | 101次/100coin, 5次/500coin, 10次/1000coin * body: count = 1 | 5 | 101次/1coin, 5次/5coin, 10次/10coin
*/ */
// public function buyLotteryTickets(Request $request): Response // public function buyLotteryTickets(Request $request): Response
// { // {
@@ -217,6 +217,7 @@ class GameController extends BaseController
} }
} }
} }
$data['tier'] = $data['reward_tier'] ?? '';
return $this->success($data); return $this->success($data);
} catch (ApiException $e) { } catch (ApiException $e) {

View File

@@ -17,9 +17,9 @@ use support\think\Db;
class GameLogic class GameLogic
{ {
public const PACKAGES = [ public const PACKAGES = [
1 => ['coin' => 100, 'paid' => 1, 'free' => 0], // 1次/100coin 1 => ['coin' => 1, 'paid' => 1, 'free' => 0], // 1次/1coin
5 => ['coin' => 500, 'paid' => 5, 'free' => 1], // 5张/500coin5购买+1赠送共6次 5 => ['coin' => 5, 'paid' => 5, 'free' => 1], // 5张/5coin5购买+1赠送共6次
10 => ['coin' => 1000, 'paid' => 10, 'free' => 3], // 10张/1000coin10购买+3赠送共13次 10 => ['coin' => 10, 'paid' => 10, 'free' => 3], // 10张/10coin10购买+3赠送共13次
]; ];
/** 钱包流水类型:购买抽奖次数 */ /** 钱包流水类型:购买抽奖次数 */
@@ -52,7 +52,7 @@ class GameLogic
throw new ApiException('Insufficient balance'); throw new ApiException('Insufficient balance');
} }
$coinAfter = $coinBefore - $cost; $coinAfter = round($coinBefore - $cost, 2);
$totalBefore = (int) ($player->total_ticket_count ?? 0); $totalBefore = (int) ($player->total_ticket_count ?? 0);
$paidBefore = (int) ($player->paid_ticket_count ?? 0); $paidBefore = (int) ($player->paid_ticket_count ?? 0);
$freeBefore = (int) ($player->free_ticket_count ?? 0); $freeBefore = (int) ($player->free_ticket_count ?? 0);
@@ -94,7 +94,7 @@ class GameLogic
DicePlayerWalletRecord::create([ DicePlayerWalletRecord::create([
'player_id' => $playerId, 'player_id' => $playerId,
'admin_id' => $adminId, 'admin_id' => $adminId,
'coin' => -$cost, 'coin' => round(-$cost, 2),
'type' => self::WALLET_TYPE_BUY_DRAW, 'type' => self::WALLET_TYPE_BUY_DRAW,
'wallet_before' => $coinBefore, 'wallet_before' => $coinBefore,
'wallet_after' => $coinAfter, 'wallet_after' => $coinAfter,
@@ -107,7 +107,7 @@ class GameLogic
DicePlayerTicketRecord::create([ DicePlayerTicketRecord::create([
'player_id' => $playerId, 'player_id' => $playerId,
'admin_id' => $adminId, 'admin_id' => $adminId,
'use_coins' => $cost, 'use_coins' => round($cost, 2),
'ante' => 1, 'ante' => 1,
'total_ticket_count' => $addTotal, 'total_ticket_count' => $addTotal,
'paid_ticket_count' => $addPaid, 'paid_ticket_count' => $addPaid,
@@ -121,7 +121,7 @@ class GameLogic
} }
return [ return [
'coin' => (float) $coinAfter, 'coin' => round((float) $coinAfter, 2),
'total_ticket_count' => (int) $totalAfter, 'total_ticket_count' => (int) $totalAfter,
'paid_ticket_count' => (int) $paidAfter, 'paid_ticket_count' => (int) $paidAfter,
'free_ticket_count' => (int) $freeAfter, 'free_ticket_count' => (int) $freeAfter,

View File

@@ -37,8 +37,8 @@ class PlayStartLogic
/** 对局状态:超时/失败 */ /** 对局状态:超时/失败 */
public const RECORD_STATUS_TIMEOUT = 0; public const RECORD_STATUS_TIMEOUT = 0;
/** 单注费用(对应原票价 100 */ /** 单注费用(抽奖券基础费用 */
private const UNIT_COST = 100; private const UNIT_COST = 1.0;
/** 免费抽奖注数缓存 key 前缀(用于强制下一局注数一致) */ /** 免费抽奖注数缓存 key 前缀(用于强制下一局注数一致) */
private const FREE_ANTE_KEY_PREFIX = 'api:game:free_ante:'; private const FREE_ANTE_KEY_PREFIX = 'api:game:free_ante:';
/** 免费抽奖注数缓存过期(秒) */ /** 免费抽奖注数缓存过期(秒) */
@@ -122,8 +122,8 @@ class PlayStartLogic
throw new ApiException('未达抽奖余额 ' . $needMinBalance . ',无法开始游戏'); throw new ApiException('未达抽奖余额 ' . $needMinBalance . ',无法开始游戏');
} }
// 付费抽奖:开始前扣除费用 ante * 100,不足则提示余额不足 // 付费抽奖:开始前扣除费用 ante * UNIT_COST,不足则提示余额不足
$paidAmount = $ticketType === self::LOTTERY_TYPE_PAID ? ($ante * self::UNIT_COST) : 0; $paidAmount = $ticketType === self::LOTTERY_TYPE_PAID ? round($ante * self::UNIT_COST, 2) : 0.0;
if ($ticketType === self::LOTTERY_TYPE_PAID && $coin < $paidAmount) { if ($ticketType === self::LOTTERY_TYPE_PAID && $coin < $paidAmount) {
throw new ApiException('余额不足'); throw new ApiException('余额不足');
} }
@@ -188,12 +188,12 @@ class PlayStartLogic
if ($isTierT5 === false && (string) ($tier ?? '') === 'T5') { if ($isTierT5 === false && (string) ($tier ?? '') === 'T5') {
$isTierT5 = true; $isTierT5 = true;
} }
// 摇色子中奖:按 dice_reward_config.real_ev 直接结算(已乘 ante;不再叠加票价 100 // 摇色子中奖:按 dice_reward_config.real_ev 直接结算(已乘 ante
$rewardWinCoin = $realEv * $ante; $rewardWinCoin = round($realEv * $ante, 2);
// 豹子判定5/30 必豹子10/15/20/25 按 DiceRewardConfig 中 BIGWIN 该点数的 weight 判定0-1000010000=100% // 豹子判定5/30 必豹子10/15/20/25 按 DiceRewardConfig 中 BIGWIN 该点数的 weight 判定0-1000010000=100%
// 杀分档位不触发豹子5/30 已在上方抽取时排除10/15/20/25 仅生成非豹子组合 // 杀分档位不触发豹子5/30 已在上方抽取时排除10/15/20/25 仅生成非豹子组合
$superWinCoin = 0; $superWinCoin = 0.0;
$isWin = 0; $isWin = 0;
$bigWinRealEv = 0.0; $bigWinRealEv = 0.0;
if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) { if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) {
@@ -223,10 +223,10 @@ class PlayStartLogic
$rollArray = $this->getSuperWinRollArray($rollNumber); $rollArray = $this->getSuperWinRollArray($rollNumber);
$isWin = 1; $isWin = 1;
$bigWinEv = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS; $bigWinEv = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS;
$superWinCoin = $bigWinEv * $ante; $superWinCoin = round($bigWinEv * $ante, 2);
// 中 BIGWIN 豹子:不走原奖励流程,不记录原奖励,不触发 T5 再来一次,仅发放豹子奖金 // 中 BIGWIN 豹子:不走原奖励流程,不记录原奖励,不触发 T5 再来一次,仅发放豹子奖金
$rewardWinCoin = 0; $rewardWinCoin = 0.0;
$realEv = 0; $realEv = 0.0;
$isTierT5 = false; $isTierT5 = false;
} else { } else {
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber); $rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
@@ -243,7 +243,7 @@ class PlayStartLogic
$startIndex, $startIndex,
$targetIndex $targetIndex
)); ));
$winCoin = $superWinCoin + $rewardWinCoin; // 赢取平台币 = 中大奖 + 摇色子中奖(豹子时 rewardWinCoin 已为 0 $winCoin = round($superWinCoin + $rewardWinCoin, 2); // 赢取平台币 = 中大奖 + 摇色子中奖(豹子时 rewardWinCoin 已为 0
$record = null; $record = null;
$configId = (int) $config->id; $configId = (int) $config->id;
@@ -302,7 +302,7 @@ class PlayStartLogic
} }
$coinBefore = (float) $p->coin; $coinBefore = (float) $p->coin;
// 开始前先扣付费金额,再加中奖金额(免费抽奖 paid_amount=0 // 开始前先扣付费金额,再加中奖金额(免费抽奖 paid_amount=0
$coinAfter = $coinBefore - $paidAmount + $winCoin; $coinAfter = round($coinBefore - $paidAmount + $winCoin, 2);
$p->coin = $coinAfter; $p->coin = $coinAfter;
// 免费抽奖消耗:优先消耗 free_ticket.count耗尽则清空 free_ticket否则兼容旧 free_ticket_count // 免费抽奖消耗:优先消耗 free_ticket.count耗尽则清空 free_ticket否则兼容旧 free_ticket_count
if ($ticketType === self::LOTTERY_TYPE_FREE) { if ($ticketType === self::LOTTERY_TYPE_FREE) {
@@ -400,13 +400,13 @@ class PlayStartLogic
$p->save(); $p->save();
// 彩金池累计盈利累加在 name=default 彩金池上: // 彩金池累计盈利累加在 name=default 彩金池上:
// 付费:每局按「本局赢取平台币 win_coin - 抽奖费用 paid_amountante*100)」 // 付费:每局按「本局赢取平台币 win_coin - 抽奖费用 paid_amountante*UNIT_COST)」
// 免费券paid_amount=0只计入 win_coin // 免费券paid_amount=0只计入 win_coin
$perPlayProfit = ($ticketType === self::LOTTERY_TYPE_PAID) ? ($winCoin - (float) $paidAmount) : $winCoin; $perPlayProfit = ($ticketType === self::LOTTERY_TYPE_PAID) ? ($winCoin - $paidAmount) : $winCoin;
$addProfit = $perPlayProfit; $addProfit = round($perPlayProfit, 2);
try { try {
DiceLotteryPoolConfig::where('id', $type0ConfigId)->update([ DiceLotteryPoolConfig::where('id', $type0ConfigId)->update([
'profit_amount' => Db::raw('IFNULL(profit_amount,0) + ' . (float) $addProfit), 'profit_amount' => Db::raw('IFNULL(profit_amount,0) + ' . sprintf('%.2f', $addProfit)),
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::warning('彩金池盈利累加失败', [ Log::warning('彩金池盈利累加失败', [
@@ -418,11 +418,11 @@ class PlayStartLogic
// 钱包流水拆分:先记录购券扣费,再记录抽奖结果(中奖/惩罚) // 钱包流水拆分:先记录购券扣费,再记录抽奖结果(中奖/惩罚)
if ($paidAmount > 0) { if ($paidAmount > 0) {
$walletAfterBuy = $coinBefore - $paidAmount; $walletAfterBuy = round($coinBefore - $paidAmount, 2);
DicePlayerWalletRecord::create([ DicePlayerWalletRecord::create([
'player_id' => $playerId, 'player_id' => $playerId,
'admin_id' => $adminId, 'admin_id' => $adminId,
'coin' => -$paidAmount, 'coin' => round(-$paidAmount, 2),
'type' => self::WALLET_TYPE_BUY_DRAW, 'type' => self::WALLET_TYPE_BUY_DRAW,
'wallet_before' => $coinBefore, 'wallet_before' => $coinBefore,
'wallet_after' => $walletAfterBuy, 'wallet_after' => $walletAfterBuy,
@@ -437,7 +437,7 @@ class PlayStartLogic
'admin_id' => $adminId, 'admin_id' => $adminId,
'coin' => $winCoin, 'coin' => $winCoin,
'type' => self::WALLET_TYPE_DRAW, 'type' => self::WALLET_TYPE_DRAW,
'wallet_before' => $walletBeforeDraw, 'wallet_before' => round($walletBeforeDraw, 2),
'wallet_after' => $coinAfter, 'wallet_after' => $coinAfter,
'remark' => $drawRemark, 'remark' => $drawRemark,
]); ]);
@@ -484,9 +484,9 @@ class PlayStartLogic
$arr['roll_number'] = is_array($arr['roll_array'] ?? null) ? array_sum($arr['roll_array']) : 0; $arr['roll_number'] = is_array($arr['roll_array'] ?? null) ? array_sum($arr['roll_array']) : 0;
$arr['reward_tier'] = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? ''); $arr['reward_tier'] = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? '');
// 记录完数据后返回当前玩家余额与抽奖次数 // 记录完数据后返回当前玩家余额与抽奖次数
$arr['coin'] = $updated ? (float) $updated->coin : 0; $arr['coin'] = $updated ? round((float) $updated->coin, 2) : 0.0;
// 本局从玩家货币中扣除的金额:付费抽奖为 ante*UNIT_COST免费抽奖为 0与 paid_amount 一致) // 本局从玩家货币中扣除的金额:付费抽奖为 ante*UNIT_COST免费抽奖为 0与 paid_amount 一致)
$arr['use_coin'] = $paidAmount; $arr['use_coin'] = round($paidAmount, 2);
return $arr; return $arr;
} }
@@ -669,9 +669,9 @@ class PlayStartLogic
$rollNumber = (int) ($chosen['grid_number'] ?? 0); $rollNumber = (int) ($chosen['grid_number'] ?? 0);
$realEv = (float) ($chosen['real_ev'] ?? 0); $realEv = (float) ($chosen['real_ev'] ?? 0);
// 摇色子中奖:按 real_ev 直接结算(与正式抽奖 run() 一致) // 摇色子中奖:按 real_ev 直接结算(与正式抽奖 run() 一致)
$rewardWinCoin = $realEv * $ante; $rewardWinCoin = round($realEv * $ante, 2);
$superWinCoin = 0; $superWinCoin = 0.0;
$isWin = 0; $isWin = 0;
$bigWinRealEv = 0.0; $bigWinRealEv = 0.0;
if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) { if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) {
@@ -699,8 +699,8 @@ class PlayStartLogic
$rollArray = $this->getSuperWinRollArray($rollNumber); $rollArray = $this->getSuperWinRollArray($rollNumber);
$isWin = 1; $isWin = 1;
$bigWinEv = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS; $bigWinEv = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS;
$superWinCoin = $bigWinEv * $ante; $superWinCoin = round($bigWinEv * $ante, 2);
$rewardWinCoin = 0; $rewardWinCoin = 0.0;
// 中豹子时不走原奖励流程 // 中豹子时不走原奖励流程
$realEv = 0.0; $realEv = 0.0;
} else { } else {
@@ -711,11 +711,11 @@ class PlayStartLogic
$rollArray = $this->generateRollArrayFromSum($rollNumber); $rollArray = $this->generateRollArrayFromSum($rollNumber);
} }
$winCoin = $superWinCoin + $rewardWinCoin; $winCoin = round($superWinCoin + $rewardWinCoin, 2);
$configId = $config !== null ? (int) $config->id : 0; $configId = $config !== null ? (int) $config->id : 0;
$configName = $config !== null ? (string) ($config->name ?? '') : '自定义'; $configName = $config !== null ? (string) ($config->name ?? '') : '自定义';
$costRealEv = $realEv + ($isWin === 1 ? $bigWinRealEv : 0.0); $costRealEv = $realEv + ($isWin === 1 ? $bigWinRealEv : 0.0);
$paidAmount = $lotteryType === 0 ? ($ante * self::UNIT_COST) : 0; $paidAmount = $lotteryType === 0 ? round($ante * self::UNIT_COST, 2) : 0.0;
$rewardTier = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? ''); $rewardTier = ($isWin === 1 && $superWinCoin > 0) ? 'BIGWIN' : (string) ($tier ?? '');
// 与写入记录的 reward_tier 完全一致:仅当展示档位为 T5非豹子大奖时触发「再来一次」链式免费局 // 与写入记录的 reward_tier 完全一致:仅当展示档位为 T5非豹子大奖时触发「再来一次」链式免费局
$grantsFreeTicket = ($rewardTier === 'T5'); $grantsFreeTicket = ($rewardTier === 'T5');

View File

@@ -59,12 +59,12 @@ class DicePlayRecordController extends BaseController
'diceLotteryPoolConfig', 'diceLotteryPoolConfig',
]); ]);
// 按当前筛选条件统计:平台总盈利 = 付费抽奖(lottery_type=0)次数×100 - 玩家总收益(win_coin 求和) // 按当前筛选条件统计:平台总盈利 = 付费金额(paid_amount 求和) - 玩家总收益(win_coin 求和)
$sumQuery = clone $query; $sumQuery = clone $query;
$playerTotalWin = (float) $sumQuery->sum('win_coin'); $playerTotalWin = (float) $sumQuery->sum('win_coin');
$paidAmountQuery = clone $query; $paidAmountQuery = clone $query;
$paidAmount = (float) $paidAmountQuery->where('lottery_type', 0)->sum('paid_amount'); $paidAmount = (float) $paidAmountQuery->where('lottery_type', 0)->sum('paid_amount');
$totalWinCoin = $paidAmount - $playerTotalWin; $totalWinCoin = round($paidAmount - $playerTotalWin, 2);
$data = $this->logic->getList($query); $data = $this->logic->getList($query);
$data['total_win_coin'] = $totalWinCoin; $data['total_win_coin'] = $totalWinCoin;

View File

@@ -30,7 +30,7 @@ class DicePlayRecordTestController extends BaseController
} }
/** /**
* 数据列表,并在结果中附带当前筛选条件下测试数据的平台总盈利 total_win_coin付费抽奖次数×100 - 玩家总收益) * 数据列表,并在结果中附带当前筛选条件下测试数据的平台总盈利 total_win_coin付费金额 paid_amount 求和 - 玩家总收益)
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
@@ -57,7 +57,7 @@ class DicePlayRecordTestController extends BaseController
$playerTotalWin = (float) $sumQuery->sum('win_coin'); $playerTotalWin = (float) $sumQuery->sum('win_coin');
$paidAmountQuery = clone $query; $paidAmountQuery = clone $query;
$paidAmount = (float) $paidAmountQuery->where('lottery_type', 0)->sum('paid_amount'); $paidAmount = (float) $paidAmountQuery->where('lottery_type', 0)->sum('paid_amount');
$totalWinCoin = $paidAmount - $playerTotalWin; $totalWinCoin = round($paidAmount - $playerTotalWin, 2);
$data = $this->logic->getList($query); $data = $this->logic->getList($query);
$data['total_win_coin'] = $totalWinCoin; $data['total_win_coin'] = $totalWinCoin;

View File

@@ -23,7 +23,7 @@ use think\model\relation\BelongsTo;
* @property $lottery_config_id 彩金池配置 * @property $lottery_config_id 彩金池配置
* @property $lottery_type 抽奖类型 * @property $lottery_type 抽奖类型
* @property $ante 底注/注数dice_ante_config.mult * @property $ante 底注/注数dice_ante_config.mult
* @property $paid_amount 付费金额(付费局=ante*100,免费局=0 * @property $paid_amount 付费金额(付费局=ante*1免费局=0
* @property $is_win 是否中大奖:豹子号[1,1,1,1,1]~[6,6,6,6,6]为1否则0 * @property $is_win 是否中大奖:豹子号[1,1,1,1,1]~[6,6,6,6,6]为1否则0
* @property $win_coin 赢取平台币(= super_win_coin + reward_win_coin * @property $win_coin 赢取平台币(= super_win_coin + reward_win_coin
* @property $super_win_coin 中大奖平台币(豹子时发放) * @property $super_win_coin 中大奖平台币(豹子时发放)

View File

@@ -22,7 +22,7 @@ use think\model\relation\BelongsTo;
* @property $is_win 中大奖:0=无,1=中奖 * @property $is_win 中大奖:0=无,1=中奖
* @property $win_coin 赢取平台币 * @property $win_coin 赢取平台币
* @property int|null $ante 底注/注数dice_ante_config.mult * @property int|null $ante 底注/注数dice_ante_config.mult
* @property int|null $paid_amount 付费金额(付费局=ante*100,免费局=0 * @property int|null $paid_amount 付费金额(付费局=ante*1免费局=0
* @property $direction 方向:0=顺时针,1=逆时针 * @property $direction 方向:0=顺时针,1=逆时针
* @property $reward_tier 中奖档位T1,T2,T3,T4,T5,BIGWIN * @property $reward_tier 中奖档位T1,T2,T3,T4,T5,BIGWIN
* @property $create_time 创建时间 * @property $create_time 创建时间
@@ -113,7 +113,7 @@ class DicePlayRecordTest extends BaseModel
} }
} }
/** 付费金额(付费局=ante*100,免费局=0 */ /** 付费金额(付费局=ante*1免费局=0 */
public function searchPaidAmountAttr($query, $value) public function searchPaidAmountAttr($query, $value)
{ {
if ($value !== '' && $value !== null) { if ($value !== '' && $value !== null) {

View File

@@ -38,7 +38,7 @@ use think\model\relation\HasMany;
* @property array|null $free_tier_weights 免费自定义档位权重 T1-T5 * @property array|null $free_tier_weights 免费自定义档位权重 T1-T5
* @property array $result_counts 落点统计 grid_number=>出现次数 * @property array $result_counts 落点统计 grid_number=>出现次数
* @property array|null $tier_counts 档位出现次数 T1=>count * @property array|null $tier_counts 档位出现次数 T1=>count
* @property float|null $platform_profit 平台赚取金额(付费抽取次数×100-玩家总收益) * @property float|null $platform_profit 平台赚取金额(付费金额 paid_amount 求和-玩家总收益)
* @property array|null $bigwin_weight 测试时 BIGWIN 档位权重快照JSONgrid_number=>weight * @property array|null $bigwin_weight 测试时 BIGWIN 档位权重快照JSONgrid_number=>weight
* @property int|null $admin_id 执行测试的管理员ID * @property int|null $admin_id 执行测试的管理员ID
* @property string|null $create_time 创建时间 * @property string|null $create_time 创建时间

View File

@@ -1,4 +1,4 @@
-- DicePlayRecord 新增注数与付费金额字段 -- DicePlayRecord 新增注数与付费金额字段
ALTER TABLE `dice_play_record` ALTER TABLE `dice_play_record`
ADD COLUMN `ante` int unsigned NOT NULL DEFAULT 1 COMMENT '底注/注数(必须为 dice_ante_config.mult 中存在的值)' AFTER `lottery_type`, ADD COLUMN `ante` int unsigned NOT NULL DEFAULT 1 COMMENT '底注/注数(必须为 dice_ante_config.mult 中存在的值)' AFTER `lottery_type`,
ADD COLUMN `paid_amount` int unsigned NOT NULL DEFAULT 0 COMMENT '付费金额(付费局=ante*100,免费局=0' AFTER `ante`; ADD COLUMN `paid_amount` int unsigned NOT NULL DEFAULT 0 COMMENT '付费金额(付费局=ante*1免费局=0' AFTER `ante`;

View File

@@ -18,11 +18,11 @@
| --- | --- | | --- | --- |
| **平台币 `coin`** | 玩家钱包余额;付费开局、购买套餐、中奖结算均围绕该字段。 | | **平台币 `coin`** | 玩家钱包余额;付费开局、购买套餐、中奖结算均围绕该字段。 |
| **注数 `ante`** | 倍数因子,须存在于表 `dice_ante_config``mult` 中;接口 `/api/game/anteConfig` 返回可选注数。 | | **注数 `ante`** | 倍数因子,须存在于表 `dice_ante_config``mult` 中;接口 `/api/game/anteConfig` 返回可选注数。 |
| **单注费用** | 付费抽奖时,开局前扣除 **`ante × 100`** 平台币(代码常量 `UNIT_COST = 100`,即「单注 100 币」口径)。 | | **单注费用** | 付费抽奖时,开局前扣除 **`ante × 1`** 平台币(代码常量 `UNIT_COST = 1`,即「单注 1 币」口径)。 |
| **方向 `direction`** | 开局参数:`0``1` 对应两套奖励数据(顺时针/逆时针或「无 / 中奖」分支,由前端与配置表共同约定);服务端在 **档位确定后**,按当前方向从 `DiceReward` 缓存结构中取该档位下的条目再按权重抽取。 | | **方向 `direction`** | 开局参数:`0``1` 对应两套奖励数据(顺时针/逆时针或「无 / 中奖」分支,由前端与配置表共同约定);服务端在 **档位确定后**,按当前方向从 `DiceReward` 缓存结构中取该档位下的条目再按权重抽取。 |
| **档位 T1T5** | 中奖层级;先抽档位,再在该档位 + 当前方向下按 `weight` 抽一条奖励配置。 | | **档位 T1T5** | 中奖层级;先抽档位,再在该档位 + 当前方向下按 `weight` 抽一条奖励配置。 |
| **`grid_number`530** | 与「五颗骰子点数之和」一致:最小 5全 1最大 30全 6用于关联奖励行与后续生成 `roll_array`。 | | **`grid_number`530** | 与「五颗骰子点数之和」一致:最小 5全 1最大 30全 6用于关联奖励行与后续生成 `roll_array`。 |
| **`real_ev`** | 奖励配置中的期望调节项;**普通中奖**结算为 **`(100 + real_ev) × ante`**(付费局在开局已扣 `ante×100`,净效果依 `real_ev` 而定)。 | | **`real_ev`** | 奖励配置中的期望调节项;**普通中奖**结算为 **`real_ev × ante`**(付费局在开局已扣 `ante×1`,净效果依 `real_ev` 而定)。 |
--- ---
@@ -33,9 +33,9 @@
2. **(可选)购买「抽奖券」套餐** 2. **(可选)购买「抽奖券」套餐**
`POST /api/game/buyLotteryTickets``count` 仅支持 `1``5``10` `POST /api/game/buyLotteryTickets``count` 仅支持 `1``5``10`
- 1100 币 → 1 次付费计数 + 0 次赠送 - 11 币 → 1 次付费计数 + 0 次赠送
- 5500 币 → 5 次付费 + **1 次赠送**(共 6 次计入总次数) - 55 币 → 5 次付费 + **1 次赠送**(共 6 次计入总次数)
- 101000 币 → 10 次付费 + **3 次赠送**(共 13 次) - 1010 币 → 10 次付费 + **3 次赠送**(共 13 次)
会更新玩家身上的 `total_ticket_count` / `paid_ticket_count` / `free_ticket_count`,并记钱包与券流水。 会更新玩家身上的 `total_ticket_count` / `paid_ticket_count` / `free_ticket_count`,并记钱包与券流水。
@@ -43,8 +43,8 @@
`POST /api/game/playStart`,需传 **`direction`0 或 1** 与 **`ante`(正整数,且须在底注配置中)**。 `POST /api/game/playStart`,需传 **`direction`0 或 1** 与 **`ante`(正整数,且须在底注配置中)**。
4. **付费 vs 免费** 4. **付费 vs 免费**
- **免费抽奖**:当 `free_ticket_count > 0` 时,本局视为免费类型:不扣 `ante×100`,但会消耗 **1 次** `free_ticket_count` - **免费抽奖**:当 `free_ticket_count > 0` 时,本局视为免费类型:不扣 `ante×1`,但会消耗 **1 次** `free_ticket_count`
- **付费抽奖**:不依赖「券张数是否大于 0」只要非免费局开局前扣 **`ante × 100`**。 - **付费抽奖**:不依赖「券张数是否大于 0」只要非免费局开局前扣 **`ante × 1`**。
> **重要**:当前实现已**不再用「抽奖券张数」作为能否开局的条件**`buyLotteryTickets` 更新的是统计与赠送次数,**真正开局仍看余额、注数、免费次数等规则**(见下节)。 > **重要**:当前实现已**不再用「抽奖券张数」作为能否开局的条件**`buyLotteryTickets` 更新的是统计与赠送次数,**真正开局仍看余额、注数、免费次数等规则**(见下节)。
@@ -61,7 +61,7 @@
- 用户存在;`ante` 合法。 - 用户存在;`ante` 合法。
- **最低余额**`coin ≥ abs(min_real_ev) × ante``min_real_ev` 来自全表 `DiceRewardConfig` 缓存),防止极端负 EV 下余额不足以覆盖风险口径。 - **最低余额**`coin ≥ abs(min_real_ev) × ante``min_real_ev` 来自全表 `DiceRewardConfig` 缓存),防止极端负 EV 下余额不足以覆盖风险口径。
- 付费局:`coin ≥ ante × 100` - 付费局:`coin ≥ ante × 1`
### 4.2 使用哪套「档位权重」:默认奖池 vs 杀分奖池 ### 4.2 使用哪套「档位权重」:默认奖池 vs 杀分奖池
@@ -89,12 +89,12 @@
### 4.4 普通奖与「豹子 / BIGWIN」 ### 4.4 普通奖与「豹子 / BIGWIN」
- 若本次抽中的 `grid_number` **不是**「豹子集合」`{5,10,15,20,25,30}`:按点数和生成 5 个 16 的骰子(和为 `grid_number`**普通奖金** = **`(100 + real_ev) × ante`**(付费局已预先扣除 `ante×100`)。 - 若本次抽中的 `grid_number` **不是**「豹子集合」`{5,10,15,20,25,30}`:按点数和生成 5 个 16 的骰子(和为 `grid_number`**普通奖金** = **`real_ev × ante`**(付费局已预先扣除 `ante×1`)。
- 若点数和落在豹子集合: - 若点数和落在豹子集合:
- **`grid_number` 为 5 或 30**:若**非**杀分路径,**必定**按豹子结算(五颗相同点数)。 - **`grid_number` 为 5 或 30**:若**非**杀分路径,**必定**按豹子结算(五颗相同点数)。
- **10 / 15 / 20 / 25**:读取 `DiceRewardConfig`**`tier = BIGWIN`** 且对应该 `grid_number` 的配置,用其 **`weight`01000010000=100%** 随机决定是否视为真豹子;否则生成**非豹子**但点数和不变的骰子组合。 - **10 / 15 / 20 / 25**:读取 `DiceRewardConfig`**`tier = BIGWIN`** 且对应该 `grid_number` 的配置,用其 **`weight`01000010000=100%** 随机决定是否视为真豹子;否则生成**非豹子**但点数和不变的骰子组合。
- **真豹子**时:奖金按 **`(100 + big_win_real_ev) × ante`** 发放(`big_win_real_ev` 来自 BIGWIN 配置;若未配则用代码兜底常量);并**不计入**当次普通 `reward_win` 那条配置(与「中豹子不走普通奖」逻辑一致,详见代码注释)。 - **真豹子**时:奖金按 **`big_win_real_ev × ante`** 发放(`big_win_real_ev` 来自 BIGWIN 配置;若未配则用代码兜底常量);并**不计入**当次普通 `reward_win` 那条配置(与「中豹子不走普通奖」逻辑一致,详见代码注释)。
杀分路径下:**不触发**豹子奖,仅展示非豹子组合。 杀分路径下:**不触发**豹子奖,仅展示非豹子组合。
@@ -106,12 +106,12 @@
**`default`** 那条池子上更新 **`profit_amount`** **`default`** 那条池子上更新 **`profit_amount`**
- **付费局**:本局贡献 `+= (本局总中奖 win_coin) - (本局付费 paid_amount)`,其中 `paid_amount = ante × 100` - **付费局**:本局贡献 `+= (本局总中奖 win_coin) - (本局付费 paid_amount)`,其中 `paid_amount = ante × 1`
- **免费局**`+= win_coin`(无票价成本,`paid_amount = 0`)。 - **免费局**`+= win_coin`(无票价成本,`paid_amount = 0`)。
该累计值与 **`safety_line``kill_enabled`** 共同决定下一局付费是否进入 **killScore** 档位权重(见 4.2)。 该累计值与 **`safety_line``kill_enabled`** 共同决定下一局付费是否进入 **killScore** 档位权重(见 4.2)。
> 注意:仓库中部分数据库迁移脚本对 `profit_amount` 的注释可能仍沿用旧口径(例如按 `100-real_ev` 解释)。当前线上行为应以 `PlayStartLogic` 中对 `profit_amount` 的实际累加逻辑为准。 > 注意:仓库中部分数据库迁移脚本对 `profit_amount` 的注释可能仍沿用旧口径。当前行为应以 `PlayStartLogic` 中对 `profit_amount` 的实际累加逻辑为准。
--- ---