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", "labelIsDefault": "Default Ante",
"placeholderName": "Please enter name", "placeholderName": "Please enter name",
"placeholderTitle": "Please enter title", "placeholderTitle": "Please enter title",
"placeholderNameAuto": "Auto from multiplier, e.g. x5",
"placeholderTitleAuto": "Auto from multiplier, e.g. x5",
"ruleNameRequired": "Please enter name", "ruleNameRequired": "Please enter name",
"ruleTitleRequired": "Please enter title", "ruleTitleRequired": "Please enter title",
"ruleMultRequired": "Please enter ante multiplier", "ruleMultRequired": "Please enter ante multiplier",

View File

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

View File

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

View File

@@ -73,8 +73,10 @@
"labelLotteryTypeFree": "Free tier pool", "labelLotteryTypeFree": "Free tier pool",
"labelAnte": "Ante", "labelAnte": "Ante",
"placeholderAnte": "Select ante config", "placeholderAnte": "Select ante config",
"placeholderPaidPool": "Leave empty for custom tier odds below (default: default)", "anteRandomOption": "Random (pick from channel ante configs)",
"placeholderFreePool": "Leave empty for custom tier odds below (default: free pool)", "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%", "tierProbHint": "Custom tier odds (T1T5), each 0100%, sum of five must not exceed 100%",
"tierFieldLabel": "Tier {tier} (%)", "tierFieldLabel": "Tier {tier} (%)",
"tierSumError": "Current sum of five tiers is {sum}%, cannot exceed 100%", "tierSumError": "Current sum of five tiers is {sum}%, cannot exceed 100%",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,13 +9,19 @@
> >
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px"> <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item :label="$t('page.form.labelName')" prop="name"> <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>
<el-form-item :label="$t('page.form.labelTitle')" prop="title"> <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>
<el-form-item :label="$t('page.form.labelMult')" prop="mult"> <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>
<el-form-item :label="$t('page.form.labelIsDefault')" prop="is_default"> <el-form-item :label="$t('page.form.labelIsDefault')" prop="is_default">
<el-radio-group v-model="formData.is_default"> <el-radio-group v-model="formData.is_default">
@@ -87,6 +93,13 @@
const formData = reactive({ ...initialFormData }) const formData = reactive({ ...initialFormData })
function syncNameTitleFromMult() {
const mult = Number(formData.mult) || 1
const label = `x${mult}`
formData.name = label
formData.title = label
}
watch( watch(
() => props.modelValue, () => props.modelValue,
async (newVal) => { async (newVal) => {
@@ -99,6 +112,7 @@
if (typeof props.data.title === 'string') formData.title = props.data.title if (typeof props.data.title === 'string') formData.title = props.data.title
formData.mult = Number(props.data.mult ?? 1) || 1 formData.mult = Number(props.data.mult ?? 1) || 1
formData.is_default = Number(props.data.is_default ?? 0) === 1 ? 1 : 0 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 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 接口 * 色子奖池配置 API 接口
@@ -20,32 +32,24 @@ export default {
* 获取 DiceLotteryPoolConfig 列表数据,含 id、name、t1_weightt5_weight用于一键测试权重档位类型下拉 * 获取 DiceLotteryPoolConfig 列表数据,含 id、name、t1_weightt5_weight用于一键测试权重档位类型下拉
* name 映射default=原 type=0killScore=原 type=1up=原 type=2 * name 映射default=原 type=0killScore=原 type=1up=原 type=2
*/ */
async getOptions(params?: Record<string, unknown>): Promise< async getOptions(params?: Record<string, unknown>): Promise<LotteryPoolConfigOption[]> {
Array<{
id: number
name: string
t1_weight: number
t2_weight: number
t3_weight: number
t4_weight: number
t5_weight: number
}>
> {
const res = await request.get<any>({ const res = await request.get<any>({
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions', url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions',
params params
}) })
const rows = Array.isArray(res) ? res : (Array.isArray((res as any)?.data) ? (res as any).data : []) const rows = Array.isArray(res) ? res : (Array.isArray((res as any)?.data) ? (res as any).data : [])
if (!Array.isArray(rows)) return [] if (!Array.isArray(rows)) return []
return rows.map((r: any) => ({ return rows.map((r: Record<string, unknown>) => {
id: Number(r.id), const base = normalizeLotteryPoolOption(r)
name: String(r.name ?? r.id ?? ''), return {
t1_weight: Number(r.t1_weight ?? 0), ...base,
t2_weight: Number(r.t2_weight ?? 0), t1_weight: Number(r.t1_weight ?? 0),
t3_weight: Number(r.t3_weight ?? 0), t2_weight: Number(r.t2_weight ?? 0),
t4_weight: Number(r.t4_weight ?? 0), t3_weight: Number(r.t3_weight ?? 0),
t5_weight: Number(r.t5_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 request from '@/utils/http'
import { normalizeLotteryPoolOption, type LotteryPoolOption } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
/** /**
* 玩家抽奖记录 API接口 * 玩家抽奖记录 API接口
@@ -59,11 +60,13 @@ export default {
}) })
}, },
/** 获取彩金池配置选项(id、name */ /** 获取彩金池配置选项(含奖池名称 */
getLotteryConfigOptions(params?: Record<string, unknown>) { async getLotteryConfigOptions(params?: Record<string, unknown>): Promise<LotteryPoolOption[]> {
return request.get<{ id: number; name: string }[]>({ const res = await request.get<any>({
url: '/core/dice/play_record/DicePlayRecord/getLotteryConfigOptions', url: '/core/dice/play_record/DicePlayRecord/getLotteryConfigOptions',
params 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 request from '@/utils/http'
import { normalizeLotteryPoolOption } from '@/views/plugin/dice/utils/lotteryPoolDisplay'
/** /**
* 大富翁-玩家 API接口 * 大富翁-玩家 API接口
@@ -84,16 +85,15 @@ export default {
}, },
/** /**
* 获取彩金池配置选项DiceLotteryPoolConfig.id、name,供 lottery_config_id 下拉使用 * 获取彩金池配置选项,供 lottery_config_id 下拉使用(含奖池名称 display_name
* @returns [ { id, 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>({ const res = await request.get<any>({
url: '/core/dice/player/DicePlayer/getLotteryConfigOptions', url: '/core/dice/player/DicePlayer/getLotteryConfigOptions',
params params
}) })
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<{ id: number; name: string }> const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<Record<string, unknown>>
return rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') })) return rows.map((r) => normalizeLotteryPoolOption(r))
}, },
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -143,6 +143,33 @@
return Number(row.paid_n_count ?? 0) 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 { function formatPlatformProfit(v: unknown): string {
const dash = t('page.detail.dash') const dash = t('page.detail.dash')
@@ -228,8 +255,16 @@
{ {
prop: 'ante', prop: 'ante',
label: 'page.table.ante', label: 'page.table.ante',
width: 90, width: 100,
align: 'center' 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', prop: 'play_again_count',

View File

@@ -20,6 +20,12 @@
<el-descriptions-item :label="$t('page.detail.paidPlannedSpins')"> <el-descriptions-item :label="$t('page.detail.paidPlannedSpins')">
{{ record.paid_planned_spins ?? $t('page.detail.dash') }} {{ record.paid_planned_spins ?? $t('page.detail.dash') }}
</el-descriptions-item> </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')"> <el-descriptions-item :label="$t('page.detail.testCount')">
{{ formatTestCountDisplay(record) }} {{ formatTestCountDisplay(record) }}
</el-descriptions-item> </el-descriptions-item>
@@ -30,10 +36,10 @@
{{ record.admin_name ?? record.admin_id ?? $t('page.detail.dash') }} {{ record.admin_name ?? record.admin_id ?? $t('page.detail.dash') }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item :label="$t('page.detail.paidPoolId')"> <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>
<el-descriptions-item :label="$t('page.detail.freePoolId')"> <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>
<el-descriptions-item :label="$t('page.detail.bigwinSnapshot')"> <el-descriptions-item :label="$t('page.detail.bigwinSnapshot')">
<template v-if="bigwinWeightDisplay.length"> <template v-if="bigwinWeightDisplay.length">
@@ -182,7 +188,7 @@
<el-option <el-option
v-for="opt in paidLotteryOptions" v-for="opt in paidLotteryOptions"
:key="opt.id" :key="opt.id"
:label="opt.name" :label="lotteryPoolOptionLabel(opt)"
:value="opt.id" :value="opt.id"
/> />
</el-select> </el-select>
@@ -199,7 +205,7 @@
<el-option <el-option
v-for="opt in freeLotteryOptions" v-for="opt in freeLotteryOptions"
:key="opt.id" :key="opt.id"
:label="opt.name" :label="lotteryPoolOptionLabel(opt)"
:value="opt.id" :value="opt.id"
/> />
</el-select> </el-select>
@@ -224,6 +230,11 @@
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue' import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
import recordApi from '../../../api/reward_config_record/index' import recordApi from '../../../api/reward_config_record/index'
import lotteryConfigApi from '../../../api/lottery_pool_config/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 { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
@@ -242,6 +253,9 @@
over_play_count?: number over_play_count?: number
chain_free_mode?: number | boolean | string chain_free_mode?: number | boolean | string
paid_planned_spins?: number paid_planned_spins?: number
ante?: number
kill_mode_enabled?: number
test_safety_line?: number
create_time?: string create_time?: string
admin_id?: number | null admin_id?: number | null
admin_name?: string admin_name?: string
@@ -278,6 +292,33 @@
return t('page.table.chainModeNo') 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 { function formatTestCountDisplay(record: RecordRow | null): string {
if (!record) return t('page.detail.dash') if (!record) return t('page.detail.dash')
const status = Number(record.status) const status = Number(record.status)
@@ -316,7 +357,18 @@
const importing = ref(false) const importing = ref(false)
const importPaidLotteryConfigId = ref<number | null>(null) const importPaidLotteryConfigId = ref<number | null>(null)
const importFreeLotteryConfigId = 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) { function tierWeightsToTableData(weightsMap: Record<string, number> | null | undefined) {
const dash = t('page.detail.dash') const dash = t('page.detail.dash')
@@ -496,6 +548,15 @@
} }
} }
watch(
() => props.modelValue,
(open) => {
if (open) {
void loadLotteryOptions()
}
}
)
function openImport() { function openImport() {
importPaidLotteryConfigId.value = importPaidLotteryConfigId.value =
props.record?.paid_lottery_config_id ?? props.record?.lottery_config_id ?? null 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 && $poolProfitTotal >= $safetyLine
&& $configKill !== null; && $configKill !== null;
$playerLinkedPool = $this->resolvePlayerLinkedPoolConfig($player, $configDeptId);
if ($ticketType === self::LOTTERY_TYPE_FREE) { if ($ticketType === self::LOTTERY_TYPE_FREE) {
// 免费抽奖券:使用本渠道 name=free 奖池档位权重;无 free 时回退 default // 免费抽奖券:使用本渠道 name=free 奖池档位权重;无 free 时回退 default
$config = $configFree ?? $configType0; $config = $configFree ?? $configType0;
@@ -151,9 +153,13 @@ class PlayStartLogic
$config = $configKill; $config = $configKill;
$usePoolWeights = true; $usePoolWeights = true;
} else { } else {
// 付费未触发杀分:按玩家 T*_weight 抽档lottery_config_id 记 default // 付费未触发杀分:关联 playerDefault 时实时读该池权重;否则按玩家行内 T*_weight
$config = $configType0; $config = $configType0;
$usePoolWeights = false; $usePoolWeights = false;
if ($playerLinkedPool !== null && $playerLinkedPool->isPlayerDefaultTemplate()) {
$usePoolWeights = true;
$config = $playerLinkedPool;
}
} }
// 按档位 T1-T5 抽取后,从 DiceReward 表按当前方向取该档位数据,再按 weight 抽取一条得到 grid_number // 按档位 T1-T5 抽取后,从 DiceReward 表按当前方向取该档位数据,再按 weight 抽取一条得到 grid_number
@@ -263,6 +269,9 @@ class PlayStartLogic
$record = null; $record = null;
$settledWinCoin = $winCoin; $settledWinCoin = $winCoin;
$configId = (int) $config->id; $configId = (int) $config->id;
if ($ticketType === self::LOTTERY_TYPE_PAID && !$usePaidKill && $playerLinkedPool !== null) {
$configId = (int) $playerLinkedPool->id;
}
$type0ConfigId = (int) $configType0->id; $type0ConfigId = (int) $configType0->id;
$rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex; // 中豹子不记录原奖励配置 id $rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex; // 中豹子不记录原奖励配置 id
$configName = (string) ($config->name ?? ''); $configName = (string) ($config->name ?? '');
@@ -789,4 +798,24 @@ class PlayStartLogic
'grants_free_ticket' => $grantsFreeTicket, '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')] #[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
public function getOptions(Request $request): Response 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'); ->order('id', 'asc');
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id')); AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $request->input('dept_id'));
$list = $query->select(); $list = $query->select();
$data = $list->map(function ($item) { $data = $list->map(function ($item) {
$row = is_array($item) ? $item : $item->toArray();
$display = DiceLotteryPoolConfig::displayLabel($row);
return [ return [
'id' => (int) $item['id'], 'id' => (int) ($row['id'] ?? 0),
'name' => (string) ($item['name'] ?? ''), 'name' => (string) ($row['name'] ?? ''),
't1_weight' => (int) ($item['t1_weight'] ?? 0), 'remark' => (string) ($row['remark'] ?? ''),
't2_weight' => (int) ($item['t2_weight'] ?? 0), 'display_name' => $display,
't3_weight' => (int) ($item['t3_weight'] ?? 0), 't1_weight' => (int) ($row['t1_weight'] ?? 0),
't4_weight' => (int) ($item['t4_weight'] ?? 0), 't2_weight' => (int) ($row['t2_weight'] ?? 0),
't5_weight' => (int) ($item['t5_weight'] ?? 0), 't3_weight' => (int) ($row['t3_weight'] ?? 0),
't4_weight' => (int) ($row['t4_weight'] ?? 0),
't5_weight' => (int) ($row['t5_weight'] ?? 0),
]; ];
})->toArray(); })->toArray();
return $this->success($data); return $this->success($data);

View File

@@ -92,7 +92,7 @@ class DicePlayRecordController extends BaseController
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')] #[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
public function getLotteryConfigOptions(Request $request): Response 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( $requestDeptId = AdminScopeHelper::pickRequestDeptId(
$request->input('dept_id'), $request->input('dept_id'),
$request->all() $request->all()
@@ -100,7 +100,13 @@ class DicePlayRecordController extends BaseController
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId); AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId);
$list = $query->select(); $list = $query->select();
$data = $list->map(function ($item) { $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(); })->toArray();
return $this->success($data); return $this->success($data);
} }

View File

@@ -43,7 +43,7 @@ class DicePlayerController extends BaseController
#[Permission('玩家列表', 'dice:player:index:index')] #[Permission('玩家列表', 'dice:player:index:index')]
public function getLotteryConfigOptions(Request $request): Response 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( $requestDeptId = AdminScopeHelper::pickRequestDeptId(
$request->input('dept_id'), $request->input('dept_id'),
$request->all() $request->all()
@@ -51,7 +51,13 @@ class DicePlayerController extends BaseController
AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId); AdminScopeHelper::applyConfigScope($query, $this->adminInfo ?? null, $requestDeptId);
$list = $query->select(); $list = $query->select();
$data = $list->map(function ($item) { $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(); })->toArray();
return $this->success($data); return $this->success($data);
} }

View File

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

View File

@@ -24,6 +24,7 @@ class DiceAnteConfigLogic extends DiceBaseLogic
public function add(array $data): mixed public function add(array $data): mixed
{ {
return $this->transaction(function () use ($data) { return $this->transaction(function () use ($data) {
$this->applyNameTitleFromMult($data);
$this->normalizeDefaultField($data); $this->normalizeDefaultField($data);
$deptId = AdminScopeHelper::resolveConfigDeptId(null, $data['dept_id'] ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT); $deptId = AdminScopeHelper::resolveConfigDeptId(null, $data['dept_id'] ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
if ((int) ($data['is_default'] ?? 0) === 1) { if ((int) ($data['is_default'] ?? 0) === 1) {
@@ -38,6 +39,7 @@ class DiceAnteConfigLogic extends DiceBaseLogic
$pickedDeptId = AdminScopeHelper::pickRequestDeptId($requestDeptId, $data); $pickedDeptId = AdminScopeHelper::pickRequestDeptId($requestDeptId, $data);
$deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $pickedDeptId); $deptId = AdminScopeHelper::resolveConfigDeptId($adminInfo, $pickedDeptId);
return $this->transaction(function () use ($id, $data, $deptId, $adminInfo, $pickedDeptId) { return $this->transaction(function () use ($id, $data, $deptId, $adminInfo, $pickedDeptId) {
$this->applyNameTitleFromMult($data);
$this->normalizeDefaultField($data); $this->normalizeDefaultField($data);
if ((int) ($data['is_default'] ?? 0) === 1) { if ((int) ($data['is_default'] ?? 0) === 1) {
$this->clearOtherDefaults((int) $id, $deptId); $this->clearOtherDefaults((int) $id, $deptId);
@@ -92,6 +94,21 @@ class DiceAnteConfigLogic extends DiceBaseLogic
$data['is_default'] = ((int) $data['is_default']) === 1 ? 1 : 0; $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 private function clearOtherDefaults(?int $excludeId = null, int $deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT): void
{ {
$query = $this->model->where('is_default', 1); $query = $this->model->where('is_default', 1);

View File

@@ -598,41 +598,100 @@ class DiceRewardLogic
if ($configCw !== null) { if ($configCw !== null) {
$tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : ''; $tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
if ($tier !== '') { if ($tier !== '') {
$rows[] = [ $rows[] = $this->buildReferenceRowFromLandingConfig(
'tier' => $tier, $tier,
'direction' => DiceReward::DIRECTION_CLOCKWISE, $configCw,
'weight' => self::WEIGHT_MIN, DiceReward::DIRECTION_CLOCKWISE,
'grid_number' => $gridNumber, $gridNumber,
'start_index' => $startId, $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,
];
} }
} }
if ($configCcw !== null) { if ($configCcw !== null) {
$tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : ''; $tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
if ($tier !== '') { if ($tier !== '') {
$rows[] = [ $rows[] = $this->buildReferenceRowFromLandingConfig(
'tier' => $tier, $tier,
'direction' => DiceReward::DIRECTION_COUNTERCLOCKWISE, $configCcw,
'weight' => self::WEIGHT_MIN, DiceReward::DIRECTION_COUNTERCLOCKWISE,
'grid_number' => $gridNumber, $gridNumber,
'start_index' => $startId, $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,
];
} }
} }
} }
return ['rows' => $rows, 'skipped' => $skipped]; 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" * 读出当前 dice_reward用于对比/复用权重。key = "direction:grid_number"
* @return array<string, array<string, mixed>> * @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; $paidN = isset($params['paid_n_count']) ? (int) $params['paid_n_count'] : 0;
$chainFreeMode = !empty($params['chain_free_mode']); $chainFreeMode = !empty($params['chain_free_mode']);
$killModeEnabled = !empty($params['kill_mode_enabled']); $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) { if ($testSafetyLine < 0) {
throw new ApiException('test_safety_line must be greater than or equal to 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) { if ($chainFreeMode) {
$tierWeightsSnapshot['chain_free_mode'] = true; $tierWeightsSnapshot['chain_free_mode'] = true;
} }
if (!empty($params['ante_random'])) {
$tierWeightsSnapshot['ante_random'] = true;
}
$record = new DiceRewardConfigRecord(); $record = new DiceRewardConfigRecord();
$plannedPaidSpins = $paidS + $paidN; $plannedPaidSpins = $paidS + $paidN;
@@ -494,6 +502,21 @@ class DiceRewardConfigRecordLogic extends DiceBaseLogic
*/ */
private function resolveWeightTestAnte(array $params, int $deptId): int 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; $anteConfigId = isset($params['ante_config_id']) ? (int) $params['ante_config_id'] : 0;
if ($anteConfigId > 0) { if ($anteConfigId > 0) {
$config = DiceAnteConfig::find($anteConfigId); $config = DiceAnteConfig::find($anteConfigId);

View File

@@ -17,7 +17,8 @@ use app\dice\model\DiceModel;
* *
* @property $id ID * @property $id ID
* @property $name 名称 * @property $name 名称
* @property $remark 备注 * @property $remark 奖池名称(后台展示名)
* @property $config_note 配置备注
* @property $safety_line 安全线 * @property $safety_line 安全线
* @property $kill_enabled 是否启用杀分0=关闭 1=开启 * @property $kill_enabled 是否启用杀分0=关闭 1=开启
* @property $create_time 创建时间 * @property $create_time 创建时间
@@ -31,6 +32,9 @@ use app\dice\model\DiceModel;
*/ */
class DiceLotteryPoolConfig extends DiceModel class DiceLotteryPoolConfig extends DiceModel
{ {
/** 玩家默认彩金池(新玩家关联;付费未杀分时运行时读取该池 T1T5 权重) */
public const NAME_PLAYER_DEFAULT = 'playerDefault';
/** /**
* 数据表主键 * 数据表主键
* @var string * @var string
@@ -43,6 +47,9 @@ class DiceLotteryPoolConfig extends DiceModel
*/ */
protected $table = 'dice_lottery_pool_config'; protected $table = 'dice_lottery_pool_config';
/** 列表/关联 JSON 附带奖池展示名 */
protected $append = ['display_name'];
/** /**
* 按名称与渠道查找奖池配置(一键测试等场景,避免命中其他渠道同名配置) * 按名称与渠道查找奖池配置(一键测试等场景,避免命中其他渠道同名配置)
*/ */
@@ -53,12 +60,57 @@ class DiceLotteryPoolConfig extends DiceModel
return $query->find(); 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) 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) public function searchLotteryConfigNameAttr($query, $value)
{ {
if ($value === '' || $value === null) { if ($value === '' || $value === null) {
return; 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)) { if (!empty($ids)) {
$query->whereIn('lottery_config_id', $ids); $query->whereIn('lottery_config_id', $ids);
} else { } else {

View File

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

View File

@@ -5,6 +5,7 @@ namespace app\dice\service;
use app\dice\helper\AdminScopeHelper; use app\dice\helper\AdminScopeHelper;
use app\dice\logic\reward\DiceRewardLogic; 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\DiceReward;
use app\dice\model\reward_config\DiceRewardConfig; use app\dice\model\reward_config\DiceRewardConfig;
use plugin\saiadmin\app\model\system\SystemDept; use plugin\saiadmin\app\model\system\SystemDept;
@@ -209,9 +210,99 @@ class DiceChannelConfigService
} }
$this->ensureRewardReferenceForDept($deptId); $this->ensureRewardReferenceForDept($deptId);
DiceRewardConfig::refreshCache($deptId); DiceRewardConfig::refreshCache($deptId);
$this->ensurePlayerDefaultPoolForDept($deptId);
$this->migratePlayersDefaultPoolToPlayerDefault($deptId);
return $result; 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 * 按业务 id 从默认模板补齐配置dice_config / dice_reward_config
*/ */
@@ -297,6 +388,7 @@ class DiceChannelConfigService
} }
$summary[$deptId] = $this->copyDefaultConfigToDept($deptId); $summary[$deptId] = $this->copyDefaultConfigToDept($deptId);
} }
$summary['_player_default_pools'] = $this->ensurePlayerDefaultPoolsAllChannels();
return $summary; return $summary;
} }

View File

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