1.新增默认彩金池配置

2.优化关联彩金池配置的名称显示
3.优化一键测试权重
4.优化底注配置
This commit is contained in:
2026-06-04 12:21:57 +08:00
parent 5d316ef7d6
commit dfb37dd33a
40 changed files with 845 additions and 177 deletions

View File

@@ -27,6 +27,8 @@
"labelIsDefault": "Default Ante",
"placeholderName": "Please enter name",
"placeholderTitle": "Please enter title",
"placeholderNameAuto": "Auto from multiplier, e.g. x5",
"placeholderTitleAuto": "Auto from multiplier, e.g. x5",
"ruleNameRequired": "Please enter name",
"ruleTitleRequired": "Please enter title",
"ruleMultRequired": "Please enter ante multiplier",

View File

@@ -5,11 +5,13 @@
"dialogTitleEdit": "Edit Lottery Pool Config",
"placeholderName": "Please enter name",
"placeholderRemark": "Please enter remark",
"placeholderPoolName": "Pool display name, e.g. Normal pool",
"placeholderConfigNote": "Optional notes about this pool",
"poolType": "Pool Type",
"placeholderPoolType": "Please select pool type",
"poolTypeNormal": "Normal",
"poolTypeFree": "Free",
"poolTypeKill": "Kill",
"poolTypeKill": "Force score kill",
"poolTypeT1": "T1 High",
"safetyLine": "Safety Line",
"t1Weight": "T1 Pool Weight (%)",
@@ -59,11 +61,13 @@
"placeholderPoolType": "Please select pool type",
"poolTypeNormal": "Normal",
"poolTypeFree": "Free",
"poolTypeKill": "Force Kill",
"poolTypeKill": "Force Score Kill",
"poolTypeT1": "T1 High Rate"
},
"table": {
"name": "Name",
"name": "Code",
"poolName": "Pool Name",
"configNote": "Remark",
"poolType": "Pool Type",
"safetyLine": "Safety Line",
"safetyLineNotUsed": "Not used for kill",

View File

@@ -22,6 +22,8 @@
"placeholderLotteryPool": "Leave empty for custom weights below, or select pool",
"currentConfig": "Current Config",
"configLabelName": "Name",
"configLabelPoolName": "Pool name",
"configLabelCode": "Code",
"configLabelType": "Type",
"configLabelWeights": "T1T5 Weights",
"configLabelRemark": "Remark",

View File

@@ -73,8 +73,10 @@
"labelLotteryTypeFree": "Free tier pool",
"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: free pool)",
"anteRandomOption": "Random (pick from channel ante configs)",
"placeholderPaidPool": "Leave empty to set T1T5 weights manually",
"placeholderFreePool": "Leave empty to set T1T5 weights manually",
"selectedPoolHint": "Selected pool: {name}",
"tierProbHint": "Custom tier odds (T1T5), each 0100%, sum of five must not exceed 100%",
"tierFieldLabel": "Tier {tier} (%)",
"tierSumError": "Current sum of five tiers is {sum}%, cannot exceed 100%",

View File

@@ -17,6 +17,8 @@
"chainModeNo": "No",
"paidPlannedSpins": "Planned paid spins",
"ante": "Ante",
"anteRandom": "Random",
"testSafetyLine": "Safety line",
"playAgainCount": "Play-again count",
"progressDraws": "{over} done",
"progressFailed": "{over} before fail",
@@ -51,11 +53,13 @@
"testCountProgress": "In progress: {over} done",
"testCountFailed": "{over} before failure",
"chainModeLabel": "Chain play-again",
"killModeOff": "Kill mode off",
"paidPlannedSpins": "Planned paid spins",
"testSafetyLine": "Test safety line",
"createTime": "Created at",
"admin": "Operator",
"paidPoolId": "Paid lottery pool config ID",
"freePoolId": "Free lottery pool config ID",
"paidPoolId": "Paid lottery pool",
"freePoolId": "Free lottery pool",
"bigwinSnapshot": "BIGWIN weight snapshot",
"sectionPaidTier": "Paid draw tier odds (T1T5, used in test)",
"sectionFreeTier": "Free draw tier odds (T1T5, used in test)",

View File

@@ -27,6 +27,8 @@
"labelIsDefault": "默认底注",
"placeholderName": "请输入名称",
"placeholderTitle": "请输入标题",
"placeholderNameAuto": "随底注倍率自动生成,如 x5",
"placeholderTitleAuto": "随底注倍率自动生成,如 x5",
"ruleNameRequired": "请输入名称",
"ruleTitleRequired": "请输入标题",
"ruleMultRequired": "请输入底注倍率",

View File

@@ -5,11 +5,14 @@
"dialogTitleEdit": "编辑色子奖池配置",
"placeholderName": "请输入名称",
"placeholderRemark": "请输入备注",
"placeholderPoolName": "请输入奖池名称,如:正常池",
"placeholderConfigNote": "选填,用于说明该奖池用途或规则",
"poolType": "奖池类型",
"poolName": "奖池名称",
"placeholderPoolType": "请选择奖池类型",
"poolTypeNormal": "正常",
"poolTypeFree": "免费",
"poolTypeKill": "强制杀",
"poolTypeKill": "强制杀",
"poolTypeT1": "T1高倍率",
"safetyLine": "安全线",
"t1Weight": "T1池权重(%)",
@@ -29,7 +32,7 @@
"tierRuleContent": "比较对象为 default 奖池的 profit_amount非单个玩家盈利。当 profit_amount 低于安全线或未开启杀分时,付费按玩家 T*_weight 抽档;当 profit_amount 高于或等于安全线且已开启杀分时,付费按 killScore 奖池抽档。免费抽奖始终按本渠道 name=free 奖池权重(无 free 时回退 default与安全线无关。",
"enableKillScore": "开启杀分",
"killScoreWeights": "杀分权重killScore",
"killWeightNote": "杀分权重请在列表中编辑 name=killScore强制杀)记录;本弹窗仅配置 default 奖池的安全线与杀分开关。",
"killWeightNote": "杀分权重请在列表中编辑 name=killScore强制杀)记录;本弹窗仅配置 default 奖池的安全线与杀分开关。",
"btnResetProfit": "重置彩金池累计盈利",
"btnSaveSafetyLine": "保存安全线与杀分开关",
"safetyLineDefaultOnlyHint": "仅 name=default正常奖池的安全线参与杀分判定其它奖池类型请勿在此配置安全线。",
@@ -55,15 +58,18 @@
},
"search": {
"poolType": "奖池类型",
"poolName": "奖池名称",
"placeholderName": "请输入名称",
"placeholderPoolType": "请选择奖池类型",
"poolTypeNormal": "正常",
"poolTypeFree": "免费",
"poolTypeKill": "强制杀",
"poolTypeKill": "强制杀",
"poolTypeT1": "T1高倍率"
},
"table": {
"name": "名称",
"name": "内部标识",
"poolName": "奖池名称",
"configNote": "备注",
"poolType": "奖池类型",
"safetyLine": "安全线",
"safetyLineNotUsed": "不参与杀分判定",

View File

@@ -22,6 +22,8 @@
"placeholderLotteryPool": "留空则使用下方自定义权重,或选择彩金池",
"currentConfig": "当前配置",
"configLabelName": "名称",
"configLabelPoolName": "奖池名称",
"configLabelCode": "内部标识",
"configLabelType": "类型",
"configLabelWeights": "T1T5 权重",
"configLabelRemark": "备注",

View File

@@ -73,8 +73,10 @@
"labelLotteryTypeFree": "免费档位奖池",
"labelAnte": "底注",
"placeholderAnte": "请选择底注配置",
"placeholderPaidPool": "不选则下方自定义档位概率(默认 default",
"placeholderFreePool": "不选则下方自定义档位概率(默认 free 免费奖池)",
"anteRandomOption": "随机(从当前渠道底注配置中抽取",
"placeholderPaidPool": "不选则下方手动设定 T1T5 档位权重",
"placeholderFreePool": "不选则下方手动设定 T1T5 档位权重",
"selectedPoolHint": "已选奖池:{name}",
"tierProbHint": "自定义档位概率T1T5每档 0-100%,五档之和不能超过 100%",
"tierFieldLabel": "档位 {tier}%",
"tierSumError": "当前五档之和为 {sum}%,不能超过 100%",

View File

@@ -17,6 +17,8 @@
"chainModeNo": "否",
"paidPlannedSpins": "计划付费次数",
"ante": "底注",
"anteRandom": "随机",
"testSafetyLine": "安全线",
"playAgainCount": "再来一次次数",
"progressDraws": "已完成 {over} 次",
"progressFailed": "失败前 {over} 次",
@@ -51,11 +53,13 @@
"testCountProgress": "进行中:已完成 {over} 次",
"testCountFailed": "失败前 {over} 次",
"chainModeLabel": "链式再来一次",
"killModeOff": "未开启杀分",
"paidPlannedSpins": "计划付费次数",
"testSafetyLine": "测试安全线",
"createTime": "创建时间",
"admin": "执行管理员",
"paidPoolId": "付费奖池配置ID",
"freePoolId": "免费奖池配置ID",
"paidPoolId": "付费彩金池",
"freePoolId": "免费彩金池",
"bigwinSnapshot": "BIGWIN 权重快照",
"sectionPaidTier": "付费抽奖档位概率T1-T5测试时使用",
"sectionFreeTier": "免费抽奖档位概率T1-T5测试时使用",

View File

@@ -9,13 +9,19 @@
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item :label="$t('page.form.labelName')" prop="name">
<el-input v-model="formData.name" :placeholder="$t('page.form.placeholderName')" />
<el-input v-model="formData.name" disabled :placeholder="$t('page.form.placeholderNameAuto')" />
</el-form-item>
<el-form-item :label="$t('page.form.labelTitle')" prop="title">
<el-input v-model="formData.title" :placeholder="$t('page.form.placeholderTitle')" />
<el-input v-model="formData.title" disabled :placeholder="$t('page.form.placeholderTitleAuto')" />
</el-form-item>
<el-form-item :label="$t('page.form.labelMult')" prop="mult">
<el-input-number v-model="formData.mult" :min="1" :step="1" style="width: 100%" />
<el-input-number
v-model="formData.mult"
:min="1"
:step="1"
style="width: 100%"
@update:model-value="syncNameTitleFromMult"
/>
</el-form-item>
<el-form-item :label="$t('page.form.labelIsDefault')" prop="is_default">
<el-radio-group v-model="formData.is_default">
@@ -87,6 +93,13 @@
const formData = reactive({ ...initialFormData })
function syncNameTitleFromMult() {
const mult = Number(formData.mult) || 1
const label = `x${mult}`
formData.name = label
formData.title = label
}
watch(
() => props.modelValue,
async (newVal) => {
@@ -99,6 +112,7 @@
if (typeof props.data.title === 'string') formData.title = props.data.title
formData.mult = Number(props.data.mult ?? 1) || 1
formData.is_default = Number(props.data.is_default ?? 0) === 1 ? 1 : 0
syncNameTitleFromMult()
}
)

View File

@@ -1,4 +1,16 @@
import request from '@/utils/http'
import {
normalizeLotteryPoolOption,
type LotteryPoolOption
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
export type LotteryPoolConfigOption = LotteryPoolOption & {
t1_weight: number
t2_weight: number
t3_weight: number
t4_weight: number
t5_weight: number
}
/**
* 色子奖池配置 API 接口
@@ -20,32 +32,24 @@ export default {
* 获取 DiceLotteryPoolConfig 列表数据,含 id、name、t1_weightt5_weight用于一键测试权重档位类型下拉
* name 映射default=原 type=0killScore=原 type=1up=原 type=2
*/
async getOptions(params?: Record<string, unknown>): Promise<
Array<{
id: number
name: string
t1_weight: number
t2_weight: number
t3_weight: number
t4_weight: number
t5_weight: number
}>
> {
async getOptions(params?: Record<string, unknown>): Promise<LotteryPoolConfigOption[]> {
const res = await request.get<any>({
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions',
params
})
const rows = Array.isArray(res) ? res : (Array.isArray((res as any)?.data) ? (res as any).data : [])
if (!Array.isArray(rows)) return []
return rows.map((r: any) => ({
id: Number(r.id),
name: String(r.name ?? r.id ?? ''),
return rows.map((r: Record<string, unknown>) => {
const base = normalizeLotteryPoolOption(r)
return {
...base,
t1_weight: Number(r.t1_weight ?? 0),
t2_weight: Number(r.t2_weight ?? 0),
t3_weight: Number(r.t3_weight ?? 0),
t4_weight: Number(r.t4_weight ?? 0),
t5_weight: Number(r.t5_weight ?? 0)
}))
}
})
},
/**

View File

@@ -1,4 +1,5 @@
import request from '@/utils/http'
import { normalizeLotteryPoolOption, type LotteryPoolOption } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
/**
* 玩家抽奖记录 API接口
@@ -59,11 +60,13 @@ export default {
})
},
/** 获取彩金池配置选项(id、name */
getLotteryConfigOptions(params?: Record<string, unknown>) {
return request.get<{ id: number; name: string }[]>({
/** 获取彩金池配置选项(含奖池名称 */
async getLotteryConfigOptions(params?: Record<string, unknown>): Promise<LotteryPoolOption[]> {
const res = await request.get<any>({
url: '/core/dice/play_record/DicePlayRecord/getLotteryConfigOptions',
params
})
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<Record<string, unknown>>
return rows.map((r) => normalizeLotteryPoolOption(r))
}
}

View File

@@ -1,4 +1,5 @@
import request from '@/utils/http'
import { normalizeLotteryPoolOption } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
/**
* 大富翁-玩家 API接口
@@ -84,16 +85,15 @@ export default {
},
/**
* 获取彩金池配置选项DiceLotteryPoolConfig.id、name,供 lottery_config_id 下拉使用
* @returns [ { id, name } ]
* 获取彩金池配置选项,供 lottery_config_id 下拉使用(含奖池名称 display_name
*/
async getLotteryConfigOptions(params?: Record<string, unknown>): Promise<Array<{ id: number; name: string }>> {
async getLotteryConfigOptions(params?: Record<string, unknown>) {
const res = await request.get<any>({
url: '/core/dice/player/DicePlayer/getLotteryConfigOptions',
params
})
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<{ id: number; name: string }>
return rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') }))
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<Record<string, unknown>>
return rows.map((r) => normalizeLotteryPoolOption(r))
},
/**

View File

@@ -87,14 +87,10 @@
getData()
}
// 奖池类型展示:按 name 映射
const typeFormatter = (row: Record<string, unknown>) => {
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 || '-'
const poolNameFormatter = (row: Record<string, unknown>) => {
const remark = String(row.remark ?? '').trim()
if (remark) return remark
return String(row.name ?? '').trim() || '-'
}
// 权重列带 %
@@ -132,8 +128,19 @@
core: {
apiFn: api.list,
columnsFactory: () => [
{ prop: 'name', label: 'page.table.name', align: 'center' },
{ prop: 'name', label: 'page.table.poolType', width: 100, align: 'center', formatter: typeFormatter },
{ prop: 'remark', label: 'page.table.poolName', minWidth: 120, align: 'center', formatter: poolNameFormatter },
{
prop: 'config_note',
label: 'page.table.configNote',
minWidth: 140,
align: 'center',
showOverflowTooltip: true,
formatter: (row: Record<string, unknown>) => {
const v = String(row.config_note ?? '').trim()
return v || '-'
}
},
{ prop: 'name', label: 'page.table.name', width: 110, align: 'center' },
{
prop: 'safety_line',
label: 'page.table.safetyLine',

View File

@@ -15,12 +15,20 @@
:disabled="dialogType === 'edit'"
/>
</el-form-item>
<el-form-item :label="$t('form.labelRemark')" prop="remark">
<el-form-item :label="$t('page.form.poolName')" prop="remark">
<el-input
v-model="formData.remark"
:placeholder="$t('page.form.placeholderPoolName')"
maxlength="200"
show-word-limit
/>
</el-form-item>
<el-form-item :label="$t('form.labelRemark')" prop="config_note">
<el-input
v-model="formData.config_note"
type="textarea"
:rows="3"
:placeholder="$t('page.form.placeholderRemark')"
:placeholder="$t('page.form.placeholderConfigNote')"
maxlength="500"
show-word-limit
/>
@@ -159,6 +167,7 @@
dept_id: undefined as number | undefined,
name: '',
remark: '',
config_note: '',
safety_line: 0 as number,
t1_weight: 0 as number,
t2_weight: 0 as number,

View File

@@ -143,6 +143,7 @@
import api from '../../api/play_record/index'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
import { lotteryPoolRowLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
const { t } = useI18n()
// 搜索表单
@@ -177,8 +178,7 @@
const usernameFormatter = (row: Record<string, any>) =>
row?.dicePlayer?.username ?? row?.player_id ?? '-'
const lotteryConfigNameFormatter = (row: Record<string, any>) =>
row?.diceLotteryPoolConfig?.name ?? row?.lottery_config_id ?? '-'
const lotteryConfigNameFormatter = (row: Record<string, any>) => lotteryPoolRowLabel(row)
const rewardTierFormatter = (row: Record<string, any>) => row?.reward_tier ?? '-'
/** 摇取点数格式化为 1,3,4,5,6,6 */

View File

@@ -37,7 +37,7 @@
<el-option
v-for="item in lotteryConfigOptions"
:key="item.id"
:label="item.name"
:label="lotteryPoolOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -180,6 +180,10 @@
<script setup lang="ts">
import api from '../../../api/play_record/index'
import { getChannelDeptRequestParams } from '@/composables/useChannelDeptScope'
import {
lotteryPoolOptionLabel,
type LotteryPoolOption
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
import type { FormInstance } from 'element-plus'
interface Props {
@@ -209,7 +213,7 @@
})
const playerOptions = ref<Array<{ id: number; username: string }>>([])
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
const lotteryConfigOptions = ref<LotteryPoolOption[]>([])
const initialFormData = {
id: null as number | null,

View File

@@ -155,6 +155,7 @@
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { lotteryPoolRowLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
const { t } = useI18n()
// 搜索表单(与 play_record 对齐:方向、赢取平台币范围、是否中大奖、中奖档位、点数和)
@@ -181,8 +182,7 @@
return res
}
const lotteryConfigNameFormatter = (row: Record<string, any>) =>
row?.diceLotteryPoolConfig?.name ?? row?.lottery_config_id ?? '-'
const lotteryConfigNameFormatter = (row: Record<string, any>) => lotteryPoolRowLabel(row)
const rewardTierFormatter = (row: Record<string, any>) => row?.reward_tier ?? '-'
/** 摇取点数格式化为 1,3,4,5,6 */

View File

@@ -148,6 +148,11 @@
getChannelDeptRequestParams,
useInjectedChannelDept
} from '@/composables/useChannelDeptScope'
import {
filterLotteryPoolOptionsByQuery,
lotteryPoolOptionLabel,
type LotteryPoolOption
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
interface Props {
modelValue: Record<string, any>
@@ -162,8 +167,8 @@
const isExpanded = ref<boolean>(false)
const channelScope = useInjectedChannelDept()
const lotteryPoolAllOptions = ref<Array<{ id: number; name: string }>>([])
const lotteryPoolOptions = ref<Array<{ id: number; name: string }>>([])
const lotteryPoolAllOptions = ref<LotteryPoolOption[]>([])
const lotteryPoolOptions = ref<LotteryPoolOption[]>([])
const lotteryPoolLoading = ref(false)
function resolveDeptParams(): Record<string, unknown> {
@@ -177,11 +182,6 @@
return {}
}
function lotteryPoolOptionLabel(item: { id: number; name: string }): string {
const name = (item.name || '').trim()
return name ? `${name} (#${item.id})` : `#${item.id}`
}
async function loadLotteryPoolOptions() {
lotteryPoolLoading.value = true
try {
@@ -197,15 +197,7 @@
}
function filterLotteryPoolOptions(query: string) {
const q = (query || '').trim().toLowerCase()
if (!q) {
lotteryPoolOptions.value = [...lotteryPoolAllOptions.value]
return
}
lotteryPoolOptions.value = lotteryPoolAllOptions.value.filter((item) => {
const name = (item.name || '').toLowerCase()
return name.includes(q) || String(item.id).includes(q)
})
lotteryPoolOptions.value = filterLotteryPoolOptionsByQuery(lotteryPoolAllOptions.value, query)
}
function onLotteryPoolDropdownVisible(visible: boolean) {

View File

@@ -115,6 +115,7 @@
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
import WalletOperateDialog from './modules/WalletOperateDialog.vue'
import { lotteryPoolRowLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
const { t } = useI18n()
const { copy } = useClipboard()
@@ -141,10 +142,13 @@
return cellValue != null && cellValue !== '' ? `${cellValue}%` : '-'
}
// 根据 lottery_config_id 显示彩金池配置名称
const lotteryConfigNameFormatter = (row: any) =>
row?.diceLotteryPoolConfig?.name ??
(row?.lottery_config_id ? `#${row.lottery_config_id}` : t('page.table.customConfig'))
const lotteryConfigNameFormatter = (row: any) => {
const label = lotteryPoolRowLabel(row)
if (label === '-' && !row?.lottery_config_id) {
return t('page.table.customConfig')
}
return label
}
// 表格
const {

View File

@@ -88,7 +88,7 @@
<el-option
v-for="item in lotteryConfigOptions"
:key="item.id"
:label="(item.name && String(item.name).trim()) || `#${item.id}`"
:label="lotteryPoolOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -97,12 +97,12 @@
<el-form-item v-if="currentLotteryConfig" :label="$t('page.form.currentConfig')" class="current-config-block">
<div class="current-lottery-config">
<div class="config-row">
<span class="config-label">{{ $t('page.form.configLabelName') }}</span>
<span>{{ currentLotteryConfig.name ?? '-' }}</span>
<span class="config-label">{{ $t('page.form.configLabelPoolName') }}</span>
<span>{{ lotteryPoolDisplayLabel(currentLotteryConfig) }}</span>
</div>
<div class="config-row">
<span class="config-label">{{ $t('page.form.configLabelType') }}</span>
<span>{{ lotteryConfigTypeText(currentLotteryConfig.name) }}</span>
<span class="config-label">{{ $t('page.form.configLabelCode') }}</span>
<span>{{ currentLotteryConfig.name ?? '-' }}</span>
</div>
<div class="config-row">
<span class="config-label">{{ $t('page.form.configLabelWeights') }}</span>
@@ -185,6 +185,7 @@
import api from '../../../api/player/index'
import lotteryConfigApi from '../../../api/lottery_pool_config/index'
import { useI18n } from 'vue-i18n'
import { lotteryPoolDisplayLabel, lotteryPoolOptionLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
import { getChannelDeptRequestParams, withChannelDeptParams } from '@/composables/useChannelDeptScope'
import { isSuperAdminUser } from '@/utils/channelLayout'
import { ElMessage } from 'element-plus'

View File

@@ -54,7 +54,7 @@
<el-option
v-for="item in lotteryConfigOptions"
:key="item.id"
:label="item.name"
:label="lotteryPoolOptionLabel(item)"
:value="item.id"
/>
</el-select>
@@ -65,6 +65,7 @@
<script setup lang="ts">
import api from '../../../api/player/index'
import { lotteryPoolOptionLabel } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
interface Props {
modelValue: Record<string, any>

View File

@@ -29,6 +29,10 @@
style="width: 100%"
@change="syncAnteFromSelect"
>
<ElOption
:label="$t('page.weightTest.anteRandomOption')"
:value="RANDOM_ANTE_CONFIG_ID"
/>
<ElOption
v-for="item in anteOptions"
:key="item.id"
@@ -85,12 +89,18 @@
style="width: 100%"
>
<ElOption
v-for="item in paidLotteryOptions"
:key="item.id"
:label="item.name"
v-for="item in lotteryOptions"
:key="'paid-pool-' + item.id"
:label="lotteryPoolOptionLabel(item)"
:value="item.id"
/>
</ElSelect>
<div v-if="selectedPaidPool" class="pool-selected-hint">
{{ $t('page.weightTest.selectedPoolHint', { name: lotteryPoolDisplayLabel(selectedPaidPool) }) }}
</div>
<div v-if="selectedPaidPool" class="pool-weights-preview">
{{ poolTierWeightsText(selectedPaidPool) }}
</div>
</ElFormItem>
<template v-if="form.paid_lottery_config_id == null">
<div class="tier-label">{{ $t('page.weightTest.tierProbHint') }}</div>
@@ -155,12 +165,18 @@
style="width: 100%"
>
<ElOption
v-for="item in freeLotteryOptions"
:key="item.id"
:label="item.name"
v-for="item in lotteryOptions"
:key="'free-pool-' + item.id"
:label="lotteryPoolOptionLabel(item)"
:value="item.id"
/>
</ElSelect>
<div v-if="selectedFreePool" class="pool-selected-hint">
{{ $t('page.weightTest.selectedPoolHint', { name: lotteryPoolDisplayLabel(selectedFreePool) }) }}
</div>
<div v-if="selectedFreePool" class="pool-weights-preview">
{{ poolTierWeightsText(selectedFreePool) }}
</div>
</ElFormItem>
<template v-if="form.free_lottery_config_id == null">
<div class="tier-label">{{ $t('page.weightTest.tierProbHintFreeChain') }}</div>
@@ -218,6 +234,13 @@
useInjectedChannelDept,
withChannelDeptParams
} from '@/composables/useChannelDeptScope'
import {
lotteryPoolDisplayLabel,
lotteryPoolOptionLabel
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
/** 底注下拉「随机」选项值(非真实 ante_config.id */
const RANDOM_ANTE_CONFIG_ID = -1
const props = defineProps<{
/** 父页面渠道栏选中值(弹窗 teleport 后 inject 可能失效) */
@@ -247,16 +270,26 @@
paid_s_count: 100,
paid_n_count: 100,
kill_mode_enabled: false,
test_safety_line: 5000
test_safety_line: 0
})
const lotteryOptions = ref<Array<{ id: number; name: string }>>([])
const paidLotteryOptions = computed(() =>
lotteryOptions.value.filter((r) => r.name === 'default')
type LotteryPoolOption = {
id: number
name: string
remark?: string
display_name?: string
t1_weight?: number
t2_weight?: number
t3_weight?: number
t4_weight?: number
t5_weight?: number
}
const lotteryOptions = ref<LotteryPoolOption[]>([])
const selectedPaidPool = computed(() =>
lotteryOptions.value.find((r) => r.id === form.paid_lottery_config_id) ?? null
)
const selectedFreePool = computed(() =>
lotteryOptions.value.find((r) => r.id === form.free_lottery_config_id) ?? null
)
const freeLotteryOptions = computed(() => {
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)
@@ -322,6 +355,15 @@
return label ? `${label} (×${item.mult})` : `×${item.mult}`
}
function poolTierWeightsText(pool: LotteryPoolOption): string {
const parts = tierKeys.map((t) => {
const key = `${t.toLowerCase()}_weight` as keyof LotteryPoolOption
const v = pool[key]
return `${t} ${v ?? 0}%`
})
return parts.join(' · ')
}
function syncAnteFromSelect() {
const opt = anteOptions.value.find((o) => o.id === form.ante_config_id)
if (opt) {
@@ -350,11 +392,13 @@
async function loadDefaultPoolInfo() {
try {
const pool = await lotteryPoolApi.getCurrentPool(resolveDeptParams())
const safetyLine = Number(pool?.safety_line ?? 0)
defaultPoolInfo.value = {
safety_line: Number(pool?.safety_line ?? 0),
safety_line: safetyLine,
kill_enabled: Number(pool?.kill_enabled ?? 1),
profit_amount: Number(pool?.profit_amount ?? 0)
}
form.test_safety_line = safetyLine
} catch {
defaultPoolInfo.value = null
}
@@ -363,12 +407,34 @@
async function loadLotteryOptions() {
try {
const list = await lotteryPoolApi.getOptions(resolveDeptParams())
lotteryOptions.value = list.map((r: { id: number; name: string }) => ({
lotteryOptions.value = list.map(
(r: {
id: number
name: string
remark?: string
display_name?: string
t1_weight?: number
t2_weight?: number
t3_weight?: number
t4_weight?: number
t5_weight?: number
}) => ({
id: r.id,
name: r.name
}))
name: r.name,
remark: r.remark,
display_name: r.display_name,
t1_weight: r.t1_weight,
t2_weight: r.t2_weight,
t3_weight: r.t3_weight,
t4_weight: r.t4_weight,
t5_weight: r.t5_weight
})
)
const playerDefault = list.find((r: { name?: string }) => r.name === 'playerDefault')
const normal = list.find((r: { name?: string }) => r.name === 'default')
if (normal) {
if (playerDefault) {
form.paid_lottery_config_id = playerDefault.id
} else if (normal) {
form.paid_lottery_config_id = normal.id
}
const freePool = list.find((r: { name?: string }) => r.name === 'free')
@@ -385,7 +451,6 @@
function buildPayload() {
const payload: Record<string, unknown> = {
ante: form.ante,
ante_config_id: form.ante_config_id,
paid_s_count: form.paid_s_count,
paid_n_count: form.paid_n_count,
free_s_count: 0,
@@ -394,6 +459,11 @@
kill_mode_enabled: form.kill_mode_enabled,
test_safety_line: form.test_safety_line
}
if (form.ante_config_id === RANDOM_ANTE_CONFIG_ID) {
payload.ante_random = true
} else {
payload.ante_config_id = form.ante_config_id
}
if (form.paid_lottery_config_id != null) {
payload.paid_lottery_config_id = form.paid_lottery_config_id
} else {
@@ -408,12 +478,15 @@
}
function validateForm(): boolean {
if (form.ante_config_id == null || form.ante_config_id <= 0) {
const isRandomAnte = form.ante_config_id === RANDOM_ANTE_CONFIG_ID
if (!isRandomAnte && (form.ante_config_id == null || form.ante_config_id <= 0)) {
ElMessage.warning(t('page.weightTest.warnAnte'))
return false
}
if (!isRandomAnte) {
syncAnteFromSelect()
if (form.ante == null || form.ante <= 0) {
}
if (!isRandomAnte && (form.ante == null || form.ante <= 0)) {
ElMessage.warning(t('page.weightTest.warnAnte'))
return false
}
@@ -563,6 +636,20 @@
border-top: 1px dashed var(--el-border-color);
}
.pool-selected-hint {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.4;
}
.pool-weights-preview {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-regular);
font-family: ui-monospace, monospace;
}
.kill-mode-field {
max-width: 280px;
}

View File

@@ -143,6 +143,33 @@
return Number(row.paid_n_count ?? 0)
}
function formatTestSafetyLine(row: Record<string, unknown>): string {
const dash = t('page.detail.dash')
if (Number(row.kill_mode_enabled ?? 0) !== 1) {
return dash
}
const line = row.test_safety_line
if (line === null || line === undefined || line === '') {
return dash
}
const n = Number(line)
return Number.isFinite(n) ? String(n) : dash
}
function formatAnteCell(row: Record<string, unknown>): string {
const ante = row.ante
if (ante === null || ante === undefined || ante === '') {
return t('page.detail.dash')
}
const snap = row.tier_weights_snapshot
const isRandom =
snap &&
typeof snap === 'object' &&
(snap as { ante_random?: boolean }).ante_random === true
const base = String(ante)
return isRandom ? `${base} (${t('page.table.anteRandom')})` : base
}
// 平台赚取金额展示(未完成或空显示 —)
function formatPlatformProfit(v: unknown): string {
const dash = t('page.detail.dash')
@@ -228,8 +255,16 @@
{
prop: 'ante',
label: 'page.table.ante',
width: 90,
align: 'center'
width: 100,
align: 'center',
formatter: (row: Record<string, unknown>) => formatAnteCell(row)
},
{
prop: 'test_safety_line',
label: 'page.table.testSafetyLine',
width: 100,
align: 'center',
formatter: (row: Record<string, unknown>) => formatTestSafetyLine(row)
},
{
prop: 'play_again_count',

View File

@@ -20,6 +20,12 @@
<el-descriptions-item :label="$t('page.detail.paidPlannedSpins')">
{{ record.paid_planned_spins ?? $t('page.detail.dash') }}
</el-descriptions-item>
<el-descriptions-item :label="$t('page.detail.testSafetyLine')">
{{ formatTestSafetyLineDetail(record) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('page.table.ante')">
{{ formatAnteDetail(record) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('page.detail.testCount')">
{{ formatTestCountDisplay(record) }}
</el-descriptions-item>
@@ -30,10 +36,10 @@
{{ record.admin_name ?? record.admin_id ?? $t('page.detail.dash') }}
</el-descriptions-item>
<el-descriptions-item :label="$t('page.detail.paidPoolId')">
{{ record.paid_lottery_config_id ?? record.lottery_config_id ?? $t('page.detail.dash') }}
{{ formatRecordPaidPoolName(record) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('page.detail.freePoolId')">
{{ record.free_lottery_config_id ?? $t('page.detail.dash') }}
{{ formatRecordFreePoolName(record) }}
</el-descriptions-item>
<el-descriptions-item :label="$t('page.detail.bigwinSnapshot')">
<template v-if="bigwinWeightDisplay.length">
@@ -182,7 +188,7 @@
<el-option
v-for="opt in paidLotteryOptions"
:key="opt.id"
:label="opt.name"
:label="lotteryPoolOptionLabel(opt)"
:value="opt.id"
/>
</el-select>
@@ -199,7 +205,7 @@
<el-option
v-for="opt in freeLotteryOptions"
:key="opt.id"
:label="opt.name"
:label="lotteryPoolOptionLabel(opt)"
:value="opt.id"
/>
</el-select>
@@ -224,6 +230,11 @@
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
import recordApi from '../../../api/reward_config_record/index'
import lotteryConfigApi from '../../../api/lottery_pool_config/index'
import {
lotteryPoolLabelById,
lotteryPoolOptionLabel,
type LotteryPoolOption
} from '@/views/plugin/dice/utils/lotteryPoolDisplay'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
@@ -242,6 +253,9 @@
over_play_count?: number
chain_free_mode?: number | boolean | string
paid_planned_spins?: number
ante?: number
kill_mode_enabled?: number
test_safety_line?: number
create_time?: string
admin_id?: number | null
admin_name?: string
@@ -278,6 +292,33 @@
return t('page.table.chainModeNo')
}
function formatTestSafetyLineDetail(record: RecordRow | null): string {
if (!record) return t('page.detail.dash')
if (Number(record.kill_mode_enabled ?? 0) !== 1) {
return t('page.detail.killModeOff')
}
const line = record.test_safety_line
if (line === null || line === undefined || line === '') {
return t('page.detail.dash')
}
return String(line)
}
function formatAnteDetail(record: RecordRow | null): string {
if (!record) return t('page.detail.dash')
const ante = record.ante
if (ante === null || ante === undefined || ante === '') {
return t('page.detail.dash')
}
const snap = record.tier_weights_snapshot
const isRandom =
snap &&
typeof snap === 'object' &&
(snap as { ante_random?: boolean }).ante_random === true
const base = String(ante)
return isRandom ? `${base} (${t('page.table.anteRandom')})` : base
}
function formatTestCountDisplay(record: RecordRow | null): string {
if (!record) return t('page.detail.dash')
const status = Number(record.status)
@@ -316,7 +357,18 @@
const importing = ref(false)
const importPaidLotteryConfigId = ref<number | null>(null)
const importFreeLotteryConfigId = ref<number | null>(null)
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
const lotteryConfigOptions = ref<LotteryPoolOption[]>([])
function formatRecordPaidPoolName(record: RecordRow | null): string {
if (!record) return t('page.detail.dash')
const id = record.paid_lottery_config_id ?? record.lottery_config_id ?? null
return lotteryPoolLabelById(id, lotteryConfigOptions.value)
}
function formatRecordFreePoolName(record: RecordRow | null): string {
if (!record) return t('page.detail.dash')
return lotteryPoolLabelById(record.free_lottery_config_id ?? null, lotteryConfigOptions.value)
}
function tierWeightsToTableData(weightsMap: Record<string, number> | null | undefined) {
const dash = t('page.detail.dash')
@@ -496,6 +548,15 @@
}
}
watch(
() => props.modelValue,
(open) => {
if (open) {
void loadLotteryOptions()
}
}
)
function openImport() {
importPaidLotteryConfigId.value =
props.record?.paid_lottery_config_id ?? props.record?.lottery_config_id ?? null

View File

@@ -0,0 +1,99 @@
/** 彩金池选项/关联行通用结构 */
export interface LotteryPoolOption {
id: number
name?: string
remark?: string
display_name?: string
}
/** 彩金池后台展示名(奖池名称):优先 display_name / remark不用内部 name 作为首选 */
export function lotteryPoolDisplayLabel(item?: LotteryPoolOption | null): string {
if (!item) {
return '-'
}
const display = String(item.display_name ?? '').trim()
if (display) {
return display
}
const remark = String(item.remark ?? '').trim()
if (remark) {
return remark
}
return String(item.name ?? '').trim() || '-'
}
/** 下拉选项文案默认仅奖池名称withId=true 时附带 ID */
export function lotteryPoolOptionLabel(
item: LotteryPoolOption,
options?: { withId?: boolean }
): string {
const label = lotteryPoolDisplayLabel(item)
if (options?.withId && item.id > 0) {
return label !== '-' ? `${label} (#${item.id})` : `#${item.id}`
}
return label !== '-' ? label : `#${item.id}`
}
/** 列表行关联彩金池(含 diceLotteryPoolConfig 关联) */
export function lotteryPoolRowLabel(row?: {
diceLotteryPoolConfig?: LotteryPoolOption | null
lottery_config_id?: number | null | string
} | null): string {
if (!row) {
return '-'
}
const pool = row.diceLotteryPoolConfig
if (pool && (pool.id || pool.remark || pool.name || pool.display_name)) {
return lotteryPoolDisplayLabel(pool)
}
const id = row.lottery_config_id
if (id !== null && id !== undefined && id !== '') {
return `#${id}`
}
return '-'
}
/** 规范化接口返回的彩金池选项 */
export function normalizeLotteryPoolOption(raw: Record<string, unknown>): LotteryPoolOption {
const id = Number(raw.id ?? 0)
const name = String(raw.name ?? '')
const remark = String(raw.remark ?? '')
const displayName = String(raw.display_name ?? '').trim()
return {
id,
name,
remark,
display_name: displayName !== '' ? displayName : remark !== '' ? remark : name
}
}
/** 按奖池名称 / 内部标识 / ID 过滤下拉 */
export function filterLotteryPoolOptionsByQuery(
list: LotteryPoolOption[],
query: string
): LotteryPoolOption[] {
const q = (query || '').trim().toLowerCase()
if (!q) {
return [...list]
}
return list.filter((item) => {
const label = lotteryPoolDisplayLabel(item).toLowerCase()
const code = String(item.name ?? '').toLowerCase()
return label.includes(q) || code.includes(q) || String(item.id).includes(q)
})
}
/** 根据 ID 从选项列表解析奖池名称 */
export function lotteryPoolLabelById(
poolId: number | null | undefined,
options: LotteryPoolOption[]
): string {
if (poolId == null || poolId <= 0) {
return '-'
}
const found = options.find((o) => o.id === poolId)
if (found) {
return lotteryPoolDisplayLabel(found)
}
return `#${poolId}`
}

View File

@@ -143,6 +143,8 @@ class PlayStartLogic
&& $poolProfitTotal >= $safetyLine
&& $configKill !== null;
$playerLinkedPool = $this->resolvePlayerLinkedPoolConfig($player, $configDeptId);
if ($ticketType === self::LOTTERY_TYPE_FREE) {
// 免费抽奖券:使用本渠道 name=free 奖池档位权重;无 free 时回退 default
$config = $configFree ?? $configType0;
@@ -151,9 +153,13 @@ class PlayStartLogic
$config = $configKill;
$usePoolWeights = true;
} else {
// 付费未触发杀分:按玩家 T*_weight 抽档lottery_config_id 记 default
// 付费未触发杀分:关联 playerDefault 时实时读该池权重;否则按玩家行内 T*_weight
$config = $configType0;
$usePoolWeights = false;
if ($playerLinkedPool !== null && $playerLinkedPool->isPlayerDefaultTemplate()) {
$usePoolWeights = true;
$config = $playerLinkedPool;
}
}
// 按档位 T1-T5 抽取后,从 DiceReward 表按当前方向取该档位数据,再按 weight 抽取一条得到 grid_number
@@ -263,6 +269,9 @@ class PlayStartLogic
$record = null;
$settledWinCoin = $winCoin;
$configId = (int) $config->id;
if ($ticketType === self::LOTTERY_TYPE_PAID && !$usePaidKill && $playerLinkedPool !== null) {
$configId = (int) $playerLinkedPool->id;
}
$type0ConfigId = (int) $configType0->id;
$rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex; // 中豹子不记录原奖励配置 id
$configName = (string) ($config->name ?? '');
@@ -789,4 +798,24 @@ class PlayStartLogic
'grants_free_ticket' => $grantsFreeTicket,
];
}
/**
* 玩家已选彩金池配置(用于 playerDefault 运行时权重)
*/
private function resolvePlayerLinkedPoolConfig(DicePlayer $player, int $configDeptId): ?DiceLotteryPoolConfig
{
$linkedId = (int) ($player->lottery_config_id ?? 0);
if ($linkedId <= 0) {
return null;
}
$cfg = DiceLotteryPoolConfig::find($linkedId);
if (!$cfg) {
return null;
}
$poolDeptId = AdminScopeHelper::normalizeRecordDeptId($cfg->dept_id ?? null);
if ($poolDeptId !== AdminScopeHelper::normalizeRecordDeptId($configDeptId)) {
return null;
}
return $cfg;
}
}

View File

@@ -38,19 +38,23 @@ class DiceLotteryPoolConfigController extends BaseController
#[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
public function getOptions(Request $request): Response
{
$query = DiceLotteryPoolConfig::field('id,name,t1_weight,t2_weight,t3_weight,t4_weight,t5_weight')
$query = DiceLotteryPoolConfig::field('id,name,remark,t1_weight,t2_weight,t3_weight,t4_weight,t5_weight')
->order('id', 'asc');
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
$list = $query->select();
$data = $list->map(function ($item) {
$row = is_array($item) ? $item : $item->toArray();
$display = DiceLotteryPoolConfig::displayLabel($row);
return [
'id' => (int) $item['id'],
'name' => (string) ($item['name'] ?? ''),
't1_weight' => (int) ($item['t1_weight'] ?? 0),
't2_weight' => (int) ($item['t2_weight'] ?? 0),
't3_weight' => (int) ($item['t3_weight'] ?? 0),
't4_weight' => (int) ($item['t4_weight'] ?? 0),
't5_weight' => (int) ($item['t5_weight'] ?? 0),
'id' => (int) ($row['id'] ?? 0),
'name' => (string) ($row['name'] ?? ''),
'remark' => (string) ($row['remark'] ?? ''),
'display_name' => $display,
't1_weight' => (int) ($row['t1_weight'] ?? 0),
't2_weight' => (int) ($row['t2_weight'] ?? 0),
't3_weight' => (int) ($row['t3_weight'] ?? 0),
't4_weight' => (int) ($row['t4_weight'] ?? 0),
't5_weight' => (int) ($row['t5_weight'] ?? 0),
];
})->toArray();
return $this->success($data);

View File

@@ -92,7 +92,7 @@ class DicePlayRecordController extends BaseController
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
public function getLotteryConfigOptions(Request $request): Response
{
$query = DiceLotteryPoolConfig::field('id,name')->order('id', 'asc');
$query = DiceLotteryPoolConfig::field('id,name,remark')->order('id', 'asc');
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
$request->input('dept_id'),
$request->all()
@@ -100,7 +100,13 @@ class DicePlayRecordController extends BaseController
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId);
$list = $query->select();
$data = $list->map(function ($item) {
return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')];
$row = is_array($item) ? $item : $item->toArray();
return [
'id' => (int) ($row['id'] ?? 0),
'name' => (string) ($row['name'] ?? ''),
'remark' => (string) ($row['remark'] ?? ''),
'display_name' => DiceLotteryPoolConfig::displayLabel($row),
];
})->toArray();
return $this->success($data);
}

View File

@@ -43,7 +43,7 @@ class DicePlayerController extends BaseController
#[Permission('玩家列表', 'dice:player:index:index')]
public function getLotteryConfigOptions(Request $request): Response
{
$query = DiceLotteryPoolConfig::field('id,name')->order('id', 'asc');
$query = DiceLotteryPoolConfig::field('id,name,remark')->order('id', 'asc');
$requestDeptId = AdminScopeHelper::pickRequestDeptId(
$request->input('dept_id'),
$request->all()
@@ -51,7 +51,13 @@ class DicePlayerController extends BaseController
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId);
$list = $query->select();
$data = $list->map(function ($item) {
return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')];
$row = is_array($item) ? $item : $item->toArray();
return [
'id' => (int) ($row['id'] ?? 0),
'name' => (string) ($row['name'] ?? ''),
'remark' => (string) ($row['remark'] ?? ''),
'display_name' => DiceLotteryPoolConfig::displayLabel($row),
];
})->toArray();
return $this->success($data);
}

View File

@@ -116,6 +116,7 @@ class DiceRewardController extends BaseController
'test_safety_line' => $post['test_safety_line'] ?? null,
'dept_id' => $post['dept_id'] ?? null,
'ante_config_id' => $post['ante_config_id'] ?? null,
'ante_random' => $post['ante_random'] ?? null,
];
$adminId = isset($this->adminInfo['id']) ? (int) $this->adminInfo['id'] : null;
$requestDeptId = AdminScopeHelper::pickRequestDeptId($request->input('dept_id'), $post);

View File

@@ -24,6 +24,7 @@ class DiceAnteConfigLogic extends DiceBaseLogic
public function add(array $data): mixed
{
return $this->transaction(function () use ($data) {
$this->applyNameTitleFromMult($data);
$this->normalizeDefaultField($data);
$deptId = AdminScopeHelper::resolveConfigDeptId(null, $data['dept_id'] ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
if ((int) ($data['is_default'] ?? 0) === 1) {
@@ -38,6 +39,7 @@ class DiceAnteConfigLogic extends DiceBaseLogic
$pickedDeptId = AdminScopeHelper::pickRequestDeptId($requestDeptId, $data);
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $pickedDeptId);
return $this->transaction(function () use ($id, $data, $deptId, $adminInfo, $pickedDeptId) {
$this->applyNameTitleFromMult($data);
$this->normalizeDefaultField($data);
if ((int) ($data['is_default'] ?? 0) === 1) {
$this->clearOtherDefaults((int) $id, $deptId);
@@ -92,6 +94,21 @@ class DiceAnteConfigLogic extends DiceBaseLogic
$data['is_default'] = ((int) $data['is_default']) === 1 ? 1 : 0;
}
/** 名称、标题随底注倍率自动设为 xN */
private function applyNameTitleFromMult(array &$data): void
{
if (!array_key_exists('mult', $data)) {
return;
}
$mult = (int) $data['mult'];
if ($mult <= 0) {
return;
}
$label = 'x' . $mult;
$data['name'] = $label;
$data['title'] = $label;
}
private function clearOtherDefaults(?int $excludeId = null, int $deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT): void
{
$query = $this->model->where('is_default', 1);

View File

@@ -598,41 +598,100 @@ class DiceRewardLogic
if ($configCw !== null) {
$tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
if ($tier !== '') {
$rows[] = [
'tier' => $tier,
'direction' => DiceReward::DIRECTION_CLOCKWISE,
'weight' => self::WEIGHT_MIN,
'grid_number' => $gridNumber,
'start_index' => $startId,
'end_index' => isset($configCw['id']) ? (int) $configCw['id'] : 0,
'ui_text' => $configCw['ui_text'] ?? '',
'real_ev' => $configCw['real_ev'] ?? null,
'remark' => $configCw['remark'] ?? '',
'type' => isset($configCw['type']) ? (int) $configCw['type'] : 0,
];
$rows[] = $this->buildReferenceRowFromLandingConfig(
$tier,
$configCw,
DiceReward::DIRECTION_CLOCKWISE,
$gridNumber,
$startId
);
}
}
if ($configCcw !== null) {
$tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
if ($tier !== '') {
$rows[] = [
'tier' => $tier,
'direction' => DiceReward::DIRECTION_COUNTERCLOCKWISE,
'weight' => self::WEIGHT_MIN,
'grid_number' => $gridNumber,
'start_index' => $startId,
'end_index' => isset($configCcw['id']) ? (int) $configCcw['id'] : 0,
'ui_text' => $configCcw['ui_text'] ?? '',
'real_ev' => $configCcw['real_ev'] ?? null,
'remark' => $configCcw['remark'] ?? '',
'type' => isset($configCcw['type']) ? (int) $configCcw['type'] : 0,
];
$rows[] = $this->buildReferenceRowFromLandingConfig(
$tier,
$configCcw,
DiceReward::DIRECTION_COUNTERCLOCKWISE,
$gridNumber,
$startId
);
}
}
}
return ['rows' => $rows, 'skipped' => $skipped];
}
/**
* 对照表落点行:档位按结算金额推断,备注与奖励配置页规则一致
*
* @param array<string, mixed> $landingConfig
* @return array<string, mixed>
*/
private function buildReferenceRowFromLandingConfig(
string $tier,
array $landingConfig,
int $direction,
int $gridNumber,
int $startId
): array {
$realEv = isset($landingConfig['real_ev']) ? (float) $landingConfig['real_ev'] : 0.0;
if ($tier !== 'BIGWIN') {
$inferred = $this->inferTierFromRealEv($realEv);
if ($inferred !== '') {
$tier = $inferred;
}
}
return [
'tier' => $tier,
'direction' => $direction,
'weight' => self::WEIGHT_MIN,
'grid_number' => $gridNumber,
'start_index' => $startId,
'end_index' => isset($landingConfig['id']) ? (int) $landingConfig['id'] : 0,
'ui_text' => $landingConfig['ui_text'] ?? '',
'real_ev' => $landingConfig['real_ev'] ?? null,
'remark' => $this->defaultRemarkForTier($tier),
'type' => isset($landingConfig['type']) ? (int) $landingConfig['type'] : 0,
];
}
/**
* 按结算金额推断档位(与前端 generateIndexByRules 一致)
*/
private function inferTierFromRealEv(float $realEv): string
{
if ($realEv > 2) {
return 'T1';
}
if ($realEv > 1) {
return 'T2';
}
if ($realEv > 0) {
return 'T3';
}
if ($realEv < 0) {
return 'T4';
}
return 'T5';
}
/**
* 档位默认备注
*/
private function defaultRemarkForTier(string $tier): string
{
return match ($tier) {
'T1', 'BIGWIN' => '大奖',
'T2' => '小赚',
'T3' => '抽水',
'T4' => '惩罚',
'T5' => '再来一次',
default => '',
};
}
/**
* 读出当前 dice_reward用于对比/复用权重。key = "direction:grid_number"
* @return array<string, array<string, mixed>>

View File

@@ -283,7 +283,12 @@ class DiceRewardConfigRecordLogic extends DiceBaseLogic
$paidN = isset($params['paid_n_count']) ? (int) $params['paid_n_count'] : 0;
$chainFreeMode = !empty($params['chain_free_mode']);
$killModeEnabled = !empty($params['kill_mode_enabled']);
$testSafetyLine = isset($params['test_safety_line']) ? (int) $params['test_safety_line'] : 5000;
if (array_key_exists('test_safety_line', $params) && $params['test_safety_line'] !== null && $params['test_safety_line'] !== '') {
$testSafetyLine = (int) $params['test_safety_line'];
} else {
$defaultPool = DiceLotteryPoolConfig::findByNameForDept('default', $deptId);
$testSafetyLine = (int) ($defaultPool->safety_line ?? 0);
}
if ($testSafetyLine < 0) {
throw new ApiException('test_safety_line must be greater than or equal to 0');
}
@@ -405,6 +410,9 @@ class DiceRewardConfigRecordLogic extends DiceBaseLogic
if ($chainFreeMode) {
$tierWeightsSnapshot['chain_free_mode'] = true;
}
if (!empty($params['ante_random'])) {
$tierWeightsSnapshot['ante_random'] = true;
}
$record = new DiceRewardConfigRecord();
$plannedPaidSpins = $paidS + $paidN;
@@ -494,6 +502,21 @@ class DiceRewardConfigRecordLogic extends DiceBaseLogic
*/
private function resolveWeightTestAnte(array $params, int $deptId): int
{
if (!empty($params['ante_random'])) {
$anteQuery = DiceAnteConfig::field('id,mult')->order('mult', 'asc');
ConfigScopeEditHelper::applyDeptIdWhere($anteQuery, $deptId);
$rows = $anteQuery->select()->toArray();
if ($rows === []) {
throw new ApiException('No ante config in current channel');
}
$picked = $rows[random_int(0, count($rows) - 1)];
$mult = (int) ($picked['mult'] ?? 0);
if ($mult <= 0) {
throw new ApiException('ANTE_MUST_POSITIVE');
}
return $mult;
}
$anteConfigId = isset($params['ante_config_id']) ? (int) $params['ante_config_id'] : 0;
if ($anteConfigId > 0) {
$config = DiceAnteConfig::find($anteConfigId);

View File

@@ -17,7 +17,8 @@ use app\dice\model\DiceModel;
*
* @property $id ID
* @property $name 名称
* @property $remark 备注
* @property $remark 奖池名称(后台展示名)
* @property $config_note 配置备注
* @property $safety_line 安全线
* @property $kill_enabled 是否启用杀分0=关闭 1=开启
* @property $create_time 创建时间
@@ -31,6 +32,9 @@ use app\dice\model\DiceModel;
*/
class DiceLotteryPoolConfig extends DiceModel
{
/** 玩家默认彩金池(新玩家关联;付费未杀分时运行时读取该池 T1T5 权重) */
public const NAME_PLAYER_DEFAULT = 'playerDefault';
/**
* 数据表主键
* @var string
@@ -43,6 +47,9 @@ class DiceLotteryPoolConfig extends DiceModel
*/
protected $table = 'dice_lottery_pool_config';
/** 列表/关联 JSON 附带奖池展示名 */
protected $append = ['display_name'];
/**
* 按名称与渠道查找奖池配置(一键测试等场景,避免命中其他渠道同名配置)
*/
@@ -53,12 +60,57 @@ class DiceLotteryPoolConfig extends DiceModel
return $query->find();
}
/**
* 是否玩家默认模板池(运行时按该池权重抽档,改池配置即对所有关联玩家生效)
*/
public function isPlayerDefaultTemplate(): bool
{
return (string) ($this->name ?? '') === self::NAME_PLAYER_DEFAULT;
}
/**
* 后台展示用奖池名称:优先 remark否则 name
*
* @param array<string, mixed>|self $row
*/
public static function displayLabel($row): string
{
// 禁止对模型实例 toArray()append display_name 会再次触发本方法,导致内存耗尽
if ($row instanceof self) {
$data = $row->getData();
$remark = trim((string) ($data['remark'] ?? ''));
if ($remark !== '') {
return $remark;
}
return trim((string) ($data['name'] ?? ''));
}
$remark = trim((string) ($row['remark'] ?? ''));
if ($remark !== '') {
return $remark;
}
return trim((string) ($row['name'] ?? ''));
}
public function getDisplayNameAttr(): string
{
$data = $this->getData();
$remark = trim((string) ($data['remark'] ?? ''));
if ($remark !== '') {
return $remark;
}
return trim((string) ($data['name'] ?? ''));
}
/**
* 名称 搜索
*/
public function searchNameAttr($query, $value)
{
$query->where('name', 'like', '%'.$value.'%');
$like = '%' . $value . '%';
$query->where(function ($q) use ($like) {
$q->where('name', 'like', $like)
->whereOr('remark', 'like', $like);
});
}
}

View File

@@ -88,13 +88,17 @@ class DicePlayRecord extends DiceModel
}
}
/** 按彩金池配置名称模糊diceLotteryPoolConfig.name */
/** 按彩金池奖池名称或内部标识模糊搜索 */
public function searchLotteryConfigNameAttr($query, $value)
{
if ($value === '' || $value === null) {
return;
}
$ids = DiceLotteryPoolConfig::where('name', 'like', '%' . $value . '%')->column('id');
$like = '%' . $value . '%';
$ids = DiceLotteryPoolConfig::where(function ($q) use ($like) {
$q->where('name', 'like', $like)
->whereOr('remark', 'like', $like);
})->column('id');
if (!empty($ids)) {
$query->whereIn('lottery_config_id', $ids);
} else {

View File

@@ -6,6 +6,7 @@
// +----------------------------------------------------------------------
namespace app\dice\model\player;
use app\dice\helper\AdminScopeHelper;
use app\dice\model\DiceModel;
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
@@ -84,44 +85,61 @@ class DicePlayer extends DiceModel
if ($name === null || $name === '') {
$model->setAttr('name', $uid);
}
// 创建玩家时:未指定则自动保存 lottery_config_id 为 DiceLotteryPoolConfig name=default 的 id,没有则为 0
// 创建玩家时:未指定则关联 name=playerDefault玩家默认彩金池,没有则为 0
try {
$lotteryConfigId = $model->getAttr('lottery_config_id');
} catch (\Throwable $e) {
$lotteryConfigId = null;
}
if ($lotteryConfigId === null || $lotteryConfigId === '' || (int) $lotteryConfigId === 0) {
$config = self::findDefaultLotteryConfigForPlayer($model);
$config = self::findPlayerDefaultLotteryConfigForPlayer($model);
$model->setAttr('lottery_config_id', $config ? (int) $config->id : 0);
}
// 彩金池权重默认取 name=default 的奖池配置
// 展示用权重从玩家关联的彩金池复制playerDefault 在抽奖时仍实时读池配置
self::setDefaultWeightsFromLotteryConfig($model);
}
/**
* 按玩家所属渠道查找 default 彩金池配置
* 按玩家所属渠道查找玩家默认彩金池name=playerDefault
*/
protected static function findDefaultLotteryConfigForPlayer(DicePlayer $model): ?DiceLotteryPoolConfig
protected static function findPlayerDefaultLotteryConfigForPlayer(DicePlayer $model): ?DiceLotteryPoolConfig
{
$query = DiceLotteryPoolConfig::where('name', 'default');
try {
$deptId = $model->getAttr('dept_id');
if ($deptId !== null && $deptId !== '' && $deptId > 0) {
$query->where('dept_id', $deptId);
}
} catch (\Throwable $e) {
// ignore
$deptId = null;
}
return $query->find();
$normalizedDeptId = AdminScopeHelper::resolvePlayerConfigDeptId($model);
if ($deptId !== null && $deptId !== '' && (int) $deptId > 0) {
$normalizedDeptId = (int) $deptId;
}
$config = DiceLotteryPoolConfig::findByNameForDept(
DiceLotteryPoolConfig::NAME_PLAYER_DEFAULT,
$normalizedDeptId
);
if ($config) {
return $config;
}
return DiceLotteryPoolConfig::findByNameForDept('default', $normalizedDeptId);
}
/**
* 从 DiceLotteryPoolConfig name=default 取 t1_weightt5_weight 作为玩家未设置时的默认值
* 从玩家关联彩金池(或 playerDefault / default取 t1_weightt5_weight 作为未设置时的默认值
*/
protected static function setDefaultWeightsFromLotteryConfig(DicePlayer $model): void
{
$config = self::findDefaultLotteryConfigForPlayer($model);
$config = null;
try {
$lotteryConfigId = (int) $model->getAttr('lottery_config_id');
} catch (\Throwable $e) {
$lotteryConfigId = 0;
}
if ($lotteryConfigId > 0) {
$config = DiceLotteryPoolConfig::find($lotteryConfigId);
}
if (!$config) {
$config = self::findPlayerDefaultLotteryConfigForPlayer($model);
}
if (!$config) {
return;
}

View File

@@ -5,6 +5,7 @@ namespace app\dice\service;
use app\dice\helper\AdminScopeHelper;
use app\dice\logic\reward\DiceRewardLogic;
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use app\dice\model\reward\DiceReward;
use app\dice\model\reward_config\DiceRewardConfig;
use plugin\saiadmin\app\model\system\SystemDept;
@@ -209,9 +210,99 @@ class DiceChannelConfigService
}
$this->ensureRewardReferenceForDept($deptId);
DiceRewardConfig::refreshCache($deptId);
$this->ensurePlayerDefaultPoolForDept($deptId);
$this->migratePlayersDefaultPoolToPlayerDefault($deptId);
return $result;
}
/**
* 为渠道补齐玩家默认彩金池 name=playerDefault权重复制自 default
*/
public function ensurePlayerDefaultPoolForDept(int $deptId): ?int
{
if (!$this->tableHasColumn('dice_lottery_pool_config', 'dept_id')) {
return null;
}
$exists = Db::table('dice_lottery_pool_config')
->where('dept_id', $deptId)
->where('name', DiceLotteryPoolConfig::NAME_PLAYER_DEFAULT)
->count();
if ($exists > 0) {
return (int) Db::table('dice_lottery_pool_config')
->where('dept_id', $deptId)
->where('name', DiceLotteryPoolConfig::NAME_PLAYER_DEFAULT)
->value('id');
}
$defaultRow = Db::table('dice_lottery_pool_config')
->where('dept_id', $deptId)
->where('name', 'default')
->find();
if (!$defaultRow) {
return null;
}
$defaultRow = (array) $defaultRow;
unset($defaultRow['id'], $defaultRow['row_id'], $defaultRow['create_time'], $defaultRow['update_time'], $defaultRow['delete_time']);
$defaultRow['name'] = DiceLotteryPoolConfig::NAME_PLAYER_DEFAULT;
$defaultRow['remark'] = '默认';
$defaultRow['safety_line'] = 0;
$defaultRow['kill_enabled'] = 0;
$defaultRow['profit_amount'] = 0;
$defaultRow['dept_id'] = $deptId;
return (int) Db::table('dice_lottery_pool_config')->insertGetId($defaultRow);
}
/**
* 将仍关联 name=default 的玩家改为关联 playerDefault杀分逻辑仍用 default 池)
*/
public function migratePlayersDefaultPoolToPlayerDefault(int $deptId): int
{
if (!$this->tableHasColumn('dice_player', 'dept_id')
|| !$this->tableHasColumn('dice_player', 'lottery_config_id')) {
return 0;
}
$playerDefaultId = Db::table('dice_lottery_pool_config')
->where('dept_id', $deptId)
->where('name', DiceLotteryPoolConfig::NAME_PLAYER_DEFAULT)
->value('id');
if (!$playerDefaultId) {
return 0;
}
$defaultPoolId = Db::table('dice_lottery_pool_config')
->where('dept_id', $deptId)
->where('name', 'default')
->value('id');
if (!$defaultPoolId) {
return 0;
}
return Db::table('dice_player')
->where('dept_id', $deptId)
->where('lottery_config_id', (int) $defaultPoolId)
->update(['lottery_config_id' => (int) $playerDefaultId]);
}
/**
* 为全部渠道与默认模板补齐 playerDefault 奖池并迁移玩家关联
*/
public function ensurePlayerDefaultPoolsAllChannels(): array
{
$deptIds = [AdminScopeHelper::DEFAULT_TEMPLATE_DEPT];
foreach (SystemDept::column('id') as $id) {
$id = (int) $id;
if ($id > 0) {
$deptIds[] = $id;
}
}
$deptIds = array_values(array_unique($deptIds));
$summary = [];
foreach ($deptIds as $deptId) {
$summary[$deptId] = [
'pool_id' => $this->ensurePlayerDefaultPoolForDept($deptId),
'players_migrated' => $this->migratePlayersDefaultPoolToPlayerDefault($deptId),
];
}
return $summary;
}
/**
* 按业务 id 从默认模板补齐配置dice_config / dice_reward_config
*/
@@ -297,6 +388,7 @@ class DiceChannelConfigService
}
$summary[$deptId] = $this->copyDefaultConfigToDept($deptId);
}
$summary['_player_default_pools'] = $this->ensurePlayerDefaultPoolsAllChannels();
return $summary;
}

View File

@@ -18,6 +18,8 @@ class DiceLotteryPoolConfigValidate extends BaseValidate
*/
protected $rule = [
'name' => 'require',
'remark' => 'max:200',
'config_note' => 'max:500',
't1_weight' => 'require',
't2_weight' => 'require',
't3_weight' => 'require',
@@ -43,6 +45,8 @@ class DiceLotteryPoolConfigValidate extends BaseValidate
protected $scene = [
'save' => [
'name',
'remark',
'config_note',
't1_weight',
't2_weight',
't3_weight',
@@ -51,6 +55,8 @@ class DiceLotteryPoolConfigValidate extends BaseValidate
],
'update' => [
'name',
'remark',
'config_note',
't1_weight',
't2_weight',
't3_weight',