优化一键测试权重

This commit is contained in:
2026-03-13 15:47:12 +08:00
parent f5eaf8da30
commit 0b26afde70
19 changed files with 991 additions and 274 deletions

View File

@@ -17,13 +17,14 @@ export default {
},
/**
* 获取 DiceLotteryPoolConfig 列表数据,含 id、name,用于 lottery_config_id 下拉
* @returns DiceLotteryPoolConfig['id','name','t1_weight'..'t5_weight'] 列表
* 获取 DiceLotteryPoolConfig 列表数据,含 id、name、type、t1_weightt5_weight用于一键测试权重档位类型下拉
* type0=付费抽奖券1=免费抽奖券;付费默认选 type=0免费默认选 type=1
*/
async getOptions(): Promise<
Array<{
id: number
name: string
type: number
t1_weight: number
t2_weight: number
t3_weight: number
@@ -34,13 +35,12 @@ export default {
const res = await request.get<any>({
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions'
})
// 兼容request.get 通常返回后端 success(data) 的 data数组部分环境可能返回整包 { data: [] }
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 ?? ''),
type: Number(r.type ?? 0),
t1_weight: Number(r.t1_weight ?? 0),
t2_weight: Number(r.t2_weight ?? 0),
t3_weight: Number(r.t3_weight ?? 0),

View File

@@ -57,9 +57,22 @@ export default {
},
/**
* 一键测试权重:创建测试记录并启动后台执行,返回 record_id 用于轮询进度
* 一键测试权重:创建测试记录并启动后台执行,按付费/免费、顺逆方向交替抽奖
* 可选 lottery_config_id不选则传 paid_tier_weights / free_tier_weightsT1-T5
*/
startWeightTest(params: { lottery_config_id: number; s_count: number; n_count: number }) {
startWeightTest(params: {
lottery_config_id?: number
paid_lottery_config_id?: number
free_lottery_config_id?: number
s_count?: number
n_count?: number
paid_s_count?: number
paid_n_count?: number
free_s_count?: number
free_n_count?: number
paid_tier_weights?: Record<string, number>
free_tier_weights?: Record<string, number>
}) {
return request.post<{ record_id: number }>({
url: '/core/dice/reward/DiceReward/startWeightTest',
data: params

View File

@@ -64,11 +64,18 @@ export default {
},
/**
* 导入:测试记录的权重写入 DiceRewardConfig 与 DiceLotteryPoolConfig,并刷新缓存
* 导入:测试记录 DiceReward、DiceRewardConfig(BIGWIN)、DiceLotteryPoolConfig(付费/免费 T1-T5)
* @param record_id 测试记录 ID
* @param lottery_config_id 可选,导入档位权重到的奖池配置 ID不传则用记录内的 lottery_config_id
* @param paid_lottery_config_id 可选,导入付费档位概率到的奖池type=0
* @param free_lottery_config_id 可选导入免费档位概率到的奖池type=1
* @param lottery_config_id 兼容旧版,不传 paid/free 时用作统一奖池
*/
importFromRecord(params: { record_id: number; lottery_config_id?: number | null }) {
importFromRecord(params: {
record_id: number
paid_lottery_config_id?: number | null
free_lottery_config_id?: number | null
lottery_config_id?: number | null
}) {
return request.post<any>({
url: '/dice/reward_config_record/DiceRewardConfigRecord/importFromRecord',
data: params

View File

@@ -8,19 +8,9 @@
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left>
<span v-if="totalWinCoin !== null" class="table-summary-inline">
测试数据玩家总收益游戏总亏损<strong>{{ totalWinCoin }}</strong>
平台总盈利<strong>{{ totalWinCoin }}</strong>
</span>
<ElSpace wrap class="table-toolbar-buttons">
<ElButton
v-permission="'dice:play_record_test:index:save'"
@click="showDialog('add')"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:add-fill" />
</template>
新增
</ElButton>
<ElButton
v-permission="'dice:play_record_test:index:destroy'"
:disabled="selectedRows.length === 0"
@@ -135,17 +125,18 @@
import EditDialog from './modules/edit-dialog.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
// 搜索表单(与 play_record 对齐:方向、赢取平台币范围、是否中大奖、中奖档位)
// 搜索表单(与 play_record 对齐:方向、赢取平台币范围、是否中大奖、中奖档位、点数和
const searchForm = ref<Record<string, unknown>>({
lottery_type: undefined,
direction: undefined,
is_win: undefined,
win_coin_min: undefined,
win_coin_max: undefined,
reward_tier: undefined
reward_tier: undefined,
roll_number: undefined
})
// 当前页 win_coin 汇总(玩家总盈利 = 游戏总亏损
// 当前筛选下平台总盈利付费抽奖次数×100 - 玩家总收益
const totalWinCoin = ref<number | null>(null)
const listApi = async (params: Record<string, any>) => {

View File

@@ -64,6 +64,23 @@
</el-select>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="点数和" prop="roll_number">
<el-select
v-model="formData.roll_number"
placeholder="全部"
clearable
style="width: 100%"
>
<el-option
v-for="n in 26"
:key="n + 4"
:label="String(n + 4)"
:value="n + 4"
/>
</el-select>
</el-form-item>
</el-col>
</sa-search-bar>
</template>

View File

@@ -2,42 +2,131 @@
<ElDialog
v-model="visible"
title="一键测试权重"
width="520px"
width="560px"
:close-on-click-modal="false"
destroy-on-close
@close="onClose"
>
<ElForm ref="formRef" :model="form" label-width="140px" :disabled="running">
<ElFormItem label="测试数据档位类型" prop="lottery_config_id" required>
<ElSelect
v-model="form.lottery_config_id"
placeholder="请选择奖池配置"
filterable
style="width: 100%"
>
<ElOption
v-for="item in lotteryOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="顺时针方向次数" prop="s_count" required>
<ElSelect v-model="form.s_count" placeholder="请选择" style="width: 100%">
<ElOption v-for="c in countOptions" :key="c" :label="String(c)" :value="c" />
</ElSelect>
</ElFormItem>
<ElFormItem label="逆时针方向次数" prop="n_count" required>
<ElSelect v-model="form.n_count" placeholder="请选择" style="width: 100%">
<ElOption v-for="c in countOptions" :key="c" :label="String(c)" :value="c" />
</ElSelect>
</ElFormItem>
<ElForm ref="formRef" :model="form" label-width="140px">
<ElSteps :active="currentStep" finish-status="success" simple class="steps-wrap">
<ElStep title="付费抽奖券" />
<ElStep title="免费抽奖券" />
</ElSteps>
<!-- 第一页付费抽奖券 -->
<div v-show="currentStep === 0" class="step-panel">
<ElFormItem label="测试数据档位类型" prop="paid_lottery_config_id">
<ElSelect
v-model="form.paid_lottery_config_id"
placeholder="不选则下方自定义档位概率(默认 type=0"
clearable
filterable
style="width: 100%"
>
<ElOption
v-for="item in paidLotteryOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<template v-if="form.paid_lottery_config_id == null || form.paid_lottery_config_id === ''">
<div class="tier-label">自定义档位概率T1T5每档 0-100%五档之和不能超过 100%</div>
<ElRow :gutter="12" class="tier-row">
<ElCol v-for="t in tierKeys" :key="'paid-' + t" :span="8">
<div class="tier-field">
<label class="tier-field-label">档位 {{ t }}%</label>
<input
type="number"
:value="getPaidTier(t)"
min="0"
max="100"
placeholder="0"
class="tier-input"
@input="setPaidTier(t, $event)"
/>
</div>
</ElCol>
</ElRow>
<div v-if="paidTierSum > 100" class="tier-error"
>当前五档之和为 {{ paidTierSum }}%不能超过 100%</div
>
</template>
<ElFormItem label="顺时针次数" prop="paid_s_count" required>
<ElSelect v-model="form.paid_s_count" placeholder="请选择" style="width: 100%">
<ElOption v-for="c in countOptions" :key="c" :label="String(c)" :value="c" />
</ElSelect>
</ElFormItem>
<ElFormItem label="逆时针次数" prop="paid_n_count" required>
<ElSelect v-model="form.paid_n_count" placeholder="请选择" style="width: 100%">
<ElOption v-for="c in countOptions" :key="c" :label="String(c)" :value="c" />
</ElSelect>
</ElFormItem>
</div>
<!-- 第二页免费抽奖券 -->
<div v-show="currentStep === 1" class="step-panel">
<ElFormItem label="测试数据档位类型" prop="free_lottery_config_id">
<ElSelect
v-model="form.free_lottery_config_id"
placeholder="不选则下方自定义档位概率(默认 type=1"
clearable
filterable
style="width: 100%"
>
<ElOption
v-for="item in freeLotteryOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<template v-if="form.free_lottery_config_id == null || form.free_lottery_config_id === ''">
<div class="tier-label">自定义档位概率T1T5每档 0-100%五档之和不能超过 100%</div>
<ElRow :gutter="12" class="tier-row">
<ElCol v-for="t in tierKeys" :key="'free-' + t" :span="8">
<div class="tier-field">
<label class="tier-field-label">档位 {{ t }}%</label>
<input
type="number"
:value="getFreeTier(t)"
min="0"
max="100"
placeholder="0"
class="tier-input"
@input="setFreeTier(t, $event)"
/>
</div>
</ElCol>
</ElRow>
<div v-if="freeTierSum > 100" class="tier-error"
>当前五档之和为 {{ freeTierSum }}%不能超过 100%</div
>
</template>
<ElFormItem label="顺时针次数" prop="free_s_count" required>
<ElSelect v-model="form.free_s_count" placeholder="请选择" style="width: 100%">
<ElOption v-for="c in countOptions" :key="c" :label="String(c)" :value="c" />
</ElSelect>
</ElFormItem>
<ElFormItem label="逆时针次数" prop="free_n_count" required>
<ElSelect v-model="form.free_n_count" placeholder="请选择" style="width: 100%">
<ElOption v-for="c in countOptions" :key="c" :label="String(c)" :value="c" />
</ElSelect>
</ElFormItem>
</div>
</ElForm>
<template #footer>
<ElButton @click="visible = false">取消</ElButton>
<ElButton type="primary" :loading="running" @click="handleStart">开始测试</ElButton>
<ElButton v-if="currentStep > 0" :disabled="running" @click="currentStep--">上一步</ElButton>
<ElButton v-if="currentStep < 1" type="primary" :disabled="running" @click="currentStep++"
>下一步</ElButton
>
<ElButton v-if="currentStep === 1" type="primary" :loading="running" @click="handleStart"
>开始测试</ElButton
>
<ElButton :disabled="running" @click="visible = false">取消</ElButton>
</template>
</ElDialog>
</template>
@@ -46,49 +135,166 @@
import api from '../../../api/reward/index'
import lotteryPoolApi from '../../../api/lottery_pool_config/index'
const countOptions = [100, 500, 1000, 5000]
const countOptions = [0, 100, 500, 1000, 5000]
const tierKeys = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
const visible = defineModel<boolean>({ default: false })
const emit = defineEmits<{ (e: 'success'): void }>()
const formRef = ref()
const currentStep = ref(0)
const form = reactive({
lottery_config_id: undefined as number | undefined,
s_count: 100,
n_count: 100
paid_lottery_config_id: undefined as number | undefined,
free_lottery_config_id: undefined as number | undefined,
paid_tier_weights: { T1: 20, T2: 20, T3: 20, T4: 20, T5: 20 } as Record<string, number>,
free_tier_weights: { T1: 20, T2: 20, T3: 20, T4: 20, T5: 20 } as Record<string, number>,
paid_s_count: 100,
paid_n_count: 100,
free_s_count: 100,
free_n_count: 100
})
const lotteryOptions = ref<Array<{ id: number; name: string; type: number }>>([])
/** 将 type 转为数字(接口可能返回字符串 "0"/"1" */
function tierTypeNum(r: { type?: number | string }): number {
const t = r.type ?? 0
return typeof t === 'number' ? t : Number(t) || 0
}
/** 付费抽奖券可选档位type=0 */
const paidLotteryOptions = computed(() =>
lotteryOptions.value.filter((r) => tierTypeNum(r) === 0)
)
/**
* 免费抽奖券可选档位:优先 type=1DiceLotteryPoolConfig.type=1若无则显示全部以便下拉有选项
*/
const freeLotteryOptions = computed(() => {
const type1List = lotteryOptions.value.filter((r) => tierTypeNum(r) === 1)
return type1List.length > 0 ? type1List : lotteryOptions.value
})
const lotteryOptions = ref<Array<{ id: number; name: string }>>([])
const running = ref(false)
function onClose() {
running.value = false
currentStep.value = 0
}
function getPaidTier(t: string): string {
const v = form.paid_tier_weights[t]
return v !== undefined && v !== null ? String(v) : ''
}
function setPaidTier(t: string, val: string | Event) {
const raw =
typeof val === 'string'
? val
: ((val as Event & { target: HTMLInputElement }).target?.value ?? '')
const num = raw === '' ? 0 : Math.max(0, Math.min(100, Number(raw) || 0))
form.paid_tier_weights[t] = num
}
const paidTierSum = computed(() =>
tierKeys.reduce((s, t) => s + (form.paid_tier_weights[t] ?? 0), 0)
)
function getFreeTier(t: string): string {
const v = form.free_tier_weights[t]
return v !== undefined && v !== null ? String(v) : ''
}
function setFreeTier(t: string, val: string | Event) {
const raw =
typeof val === 'string'
? val
: ((val as Event & { target: HTMLInputElement }).target?.value ?? '')
const num = raw === '' ? 0 : Math.max(0, Math.min(100, Number(raw) || 0))
form.free_tier_weights[t] = num
}
const freeTierSum = computed(() =>
tierKeys.reduce((s, t) => s + (form.free_tier_weights[t] ?? 0), 0)
)
async function loadLotteryOptions() {
try {
const list = await lotteryPoolApi.getOptions()
lotteryOptions.value = list.map((r: { id: number; name: string }) => ({ id: r.id, name: r.name }))
if (list.length && !form.lottery_config_id) {
form.lottery_config_id = list[0].id
lotteryOptions.value = list.map(
(r: { id: number; name: string; type?: number | string }) => ({
id: r.id,
name: r.name,
type: tierTypeNum(r)
})
)
// 付费抽奖券默认使用 type=0 的档位类型
const type0 = list.find((r: { type?: number | string }) => tierTypeNum(r) === 0)
if (type0) {
form.paid_lottery_config_id = type0.id
}
// 免费抽奖券默认使用 type=1 的档位类型DiceLotteryPoolConfig.type=1若无 type=1 则默认选第一项
const type1 = list.find((r: { type?: number | string }) => tierTypeNum(r) === 1)
if (type1) {
form.free_lottery_config_id = type1.id
} else if (list.length > 0) {
form.free_lottery_config_id = list[0].id
}
} catch (_) {
lotteryOptions.value = []
}
}
async function handleStart() {
if (!form.lottery_config_id) {
ElMessage.warning('请选择测试数据档位类型')
return
function buildPayload() {
const payload: Record<string, unknown> = {
paid_s_count: form.paid_s_count,
paid_n_count: form.paid_n_count,
free_s_count: form.free_s_count,
free_n_count: form.free_n_count
}
if (form.paid_lottery_config_id != null && form.paid_lottery_config_id !== '') {
payload.paid_lottery_config_id = form.paid_lottery_config_id
} else {
payload.paid_tier_weights = { ...form.paid_tier_weights }
}
if (form.free_lottery_config_id != null && form.free_lottery_config_id !== '') {
payload.free_lottery_config_id = form.free_lottery_config_id
} else {
payload.free_tier_weights = { ...form.free_tier_weights }
}
return payload
}
function validateForm(): boolean {
if (form.paid_s_count + form.paid_n_count + form.free_s_count + form.free_n_count <= 0) {
ElMessage.warning('付费或免费至少一种方向次数之和大于 0')
return false
}
const needPaidTier = form.paid_lottery_config_id == null || form.paid_lottery_config_id === ''
const needFreeTier = form.free_lottery_config_id == null || form.free_lottery_config_id === ''
if (needPaidTier) {
const sum = paidTierSum.value
if (sum <= 0) {
ElMessage.warning('付费未选奖池时T1T5 档位概率之和需大于 0')
return false
}
if (sum > 100) {
ElMessage.warning('付费档位概率 T1T5 之和不能超过 100%')
return false
}
}
if (needFreeTier) {
const sum = freeTierSum.value
if (sum <= 0) {
ElMessage.warning('免费未选奖池时T1T5 档位概率之和需大于 0')
return false
}
if (sum > 100) {
ElMessage.warning('免费档位概率 T1T5 之和不能超过 100%')
return false
}
}
return true
}
async function handleStart() {
if (!validateForm()) return
running.value = true
try {
await api.startWeightTest({
lottery_config_id: form.lottery_config_id,
s_count: form.s_count,
n_count: form.n_count
})
ElMessage.success('测试任务已创建,后台将自动执行。请在【玩家抽奖记录(测试数据)】中查看生成的测试数据')
await api.startWeightTest(buildPayload())
ElMessage.success(
'测试任务已创建,后台将自动执行。请在【玩家抽奖记录(测试数据)】中查看生成的测试数据'
)
visible.value = false
emit('success')
} catch (e: any) {
@@ -105,4 +311,71 @@
onClose()
}
})
// 切换到免费步骤时,若当前选中 id 不在免费档位列表中,则重置为第一个 type=1 的选项,避免显示错误
watch(currentStep, (step) => {
if (step === 1) {
const freeOpts = freeLotteryOptions.value
const id = form.free_lottery_config_id
if (freeOpts.length && (id == null || !freeOpts.some((o) => o.id === id))) {
form.free_lottery_config_id = freeOpts[0].id
}
}
})
</script>
<style lang="scss" scoped>
.steps-wrap {
margin-bottom: 16px;
}
.step-panel {
min-height: 200px;
}
.tier-label {
font-size: 13px;
color: var(--el-text-color-secondary);
margin-bottom: 8px;
}
.tier-row {
margin-bottom: 12px;
}
.tier-field {
margin-bottom: 12px;
}
.tier-field-label {
display: block;
font-size: 14px;
color: var(--el-text-color-regular);
margin-bottom: 4px;
line-height: 1.5;
}
.tier-input {
display: block;
width: 100%;
padding: 8px 12px;
font-size: 14px;
line-height: 1.5;
color: var(--el-text-color-regular);
background-color: var(--el-fill-color-blank);
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-base);
box-sizing: border-box;
}
.tier-input:hover {
border-color: var(--el-border-color-hover);
}
.tier-input:focus {
outline: none;
border-color: var(--el-color-primary);
box-shadow: 0 0 0 2px var(--el-color-primary-light-7);
}
.tier-input::placeholder {
color: var(--el-text-color-placeholder);
}
.tier-error {
font-size: 12px;
color: var(--el-color-danger);
margin-top: 4px;
margin-bottom: 8px;
}
</style>

View File

@@ -8,16 +8,6 @@
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left>
<ElSpace wrap>
<ElButton
v-permission="'dice:reward_config_record:index:save'"
@click="showDialog('add')"
v-ripple
>
<template #icon>
<ArtSvgIcon icon="ri:add-fill" />
</template>
新增
</ElButton>
<ElButton
v-permission="'dice:reward_config_record:index:destroy'"
:disabled="selectedRows.length === 0"
@@ -46,13 +36,21 @@
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
>
<!-- 权重快照显示条数 -->
<template #snapshot_summary="{ row }">
<span>{{ getSnapshotSummary(row) }}</span>
<!-- 状态-1失败 0测试中 1完成 -->
<template #status="{ row }">
<span>{{ formatStatus(row.status) }}</span>
</template>
<!-- 落点统计显示总次数 -->
<template #result_summary="{ row }">
<span>{{ getResultSummary(row) }}</span>
<!-- 付费抽取顺时针逆时针抽取次数兼容旧数据用 s_count/n_count -->
<template #paid_draw="{ row }">
<span> {{ getPaidS(row) }} / {{ getPaidN(row) }}</span>
</template>
<!-- 免费抽取顺时针逆时针抽取次数 -->
<template #free_draw="{ row }">
<span> {{ row.free_s_count ?? 0 }} / {{ row.free_n_count ?? 0 }}</span>
</template>
<!-- 平台赚取金额 -->
<template #platform_profit="{ row }">
<span>{{ formatPlatformProfit(row.platform_profit) }}</span>
</template>
<!-- 操作列 -->
<template #operation="{ row }">
@@ -80,8 +78,8 @@
:data="dialogData"
@success="refreshData"
/>
<!-- 详情抽屉 -->
<DetailDrawer v-model="detailVisible" :record="detailRecord" />
<!-- 详情抽屉导入成功后刷新列表 -->
<DetailDrawer v-model="detailVisible" :record="detailRecord" @import-done="refreshData" />
</div>
</template>
@@ -102,49 +100,52 @@
getData()
}
// 详情抽屉
// 详情抽屉:打开时拉取完整记录(含 paid_tier_weights、free_tier_weights 等)
const detailVisible = ref(false)
const detailRecord = ref<Record<string, any> | null>(null)
function openDetail(row: Record<string, any>) {
async function openDetail(row: Record<string, any>) {
const id = row?.id
if (id == null) return
detailRecord.value = { ...row }
detailVisible.value = true
try {
const res = await api.read(id)
const data = res?.data ?? res
if (data && typeof data === 'object') {
detailRecord.value = data
}
} catch {
// 读取失败时保留列表行数据
}
}
// 权重快照摘要:条数(兼容后端返回数组或字符串
function getSnapshotSummary(row: Record<string, any>): string {
const snap = row.weight_config_snapshot
if (Array.isArray(snap)) return `${snap.length}`
if (snap && typeof snap === 'object') return `${Object.keys(snap).length}`
if (typeof snap === 'string') {
try {
const arr = JSON.parse(snap)
return Array.isArray(arr) ? `${arr.length}` : '—'
} catch {
return '—'
}
}
// 状态文案:-1失败 0测试中 1完成2=执行中也显示测试中
function formatStatus(status: unknown): string {
const s = Number(status)
if (s === -1) return '失败'
if (s === 1) return '完成'
if (s === 0 || s === 2) return '测试中'
return '—'
}
// 落点统计摘要:总次数
function getResultSummary(row: Record<string, any>): string {
const counts = row.result_counts
if (counts && typeof counts === 'object' && !Array.isArray(counts)) {
const total = Object.values(counts).reduce((s: number, v: any) => s + (Number(v) || 0), 0)
return `${total}`
}
if (typeof counts === 'string') {
try {
const obj = JSON.parse(counts)
if (obj && typeof obj === 'object') {
const total = Object.values(obj).reduce((s: number, v: any) => s + (Number(v) || 0), 0)
return `${total}`
}
} catch {
// ignore
}
}
return '—'
// 付费抽取次数(兼容旧数据:无 paid_s_count 时用 s_count
function getPaidS(row: Record<string, any>): number {
const v = row.paid_s_count
if (v !== undefined && v !== null && (Number(v) || 0) > 0) return Number(v)
return Number(row.s_count ?? 0)
}
function getPaidN(row: Record<string, any>): number {
const v = row.paid_n_count
if (v !== undefined && v !== null && (Number(v) || 0) > 0) return Number(v)
return Number(row.n_count ?? 0)
}
// 平台赚取金额展示(未完成或空显示 —)
function formatPlatformProfit(v: unknown): string {
if (v === null || v === undefined || v === '') return '—'
const n = Number(v)
if (Number.isNaN(n)) return '—'
return String(n)
}
// 表格配置
@@ -173,39 +174,34 @@
label: '状态',
width: 90,
align: 'center',
formatter: (row: Record<string, any>) =>
row.status === 1 ? '完成' : row.status === -1 ? '失败' : '待完成'
useSlot: true
},
{ prop: 's_count', label: '顺时针次数', width: 110, align: 'center' },
{ prop: 'n_count', label: '逆时针次数', width: 110, align: 'center' },
{ prop: 'test_count', label: '测试总次数', width: 110, align: 'center' },
{ prop: 'over_play_count', label: '完成次数', width: 110, align: 'center' },
{ prop: 'lottery_config_id', label: '奖池配置ID', width: 110, align: 'center' },
{
prop: 'admin_name',
label: '管理员',
prop: 'paid_draw',
label: '付费抽取',
width: 160,
align: 'center',
useSlot: true
},
{
prop: 'free_draw',
label: '免费抽取',
width: 160,
align: 'center',
useSlot: true
},
{
prop: 'platform_profit',
label: '平台赚取金额',
width: 120,
align: 'center',
showOverflowTooltip: true
},
{
prop: 'snapshot_summary',
label: '权重快照',
width: 110,
align: 'center',
useSlot: true
},
{ prop: 'total_play_count', label: '总抽奖次数', width: 110, align: 'center' },
{
prop: 'result_summary',
label: '落点统计',
width: 150,
align: 'center',
useSlot: true
},
{
prop: 'remark',
label: '备注',
minWidth: 140,
prop: 'admin_name',
label: '创建管理员',
width: 120,
align: 'center',
showOverflowTooltip: true
},
@@ -227,7 +223,6 @@
dialogType,
dialogVisible,
dialogData,
showDialog,
deleteRow,
deleteSelectedRows,
handleSelectionChange,

View File

@@ -21,31 +21,51 @@
<el-descriptions-item label="执行管理员">
{{ record.admin_name ?? record.admin_id ?? '—' }}
</el-descriptions-item>
<el-descriptions-item label="奖池配置ID" :span="2">
{{ record.lottery_config_id ?? '—' }}
<el-descriptions-item label="付费奖池配置ID">
{{ record.paid_lottery_config_id ?? record.lottery_config_id ?? '—' }}
</el-descriptions-item>
<el-descriptions-item label="免费奖池配置ID">
{{ record.free_lottery_config_id ?? '—' }}
</el-descriptions-item>
</el-descriptions>
</div>
<div class="detail-section">
<div class="section-title">T1-T5 档位权重配比测试时使用的奖池配比</div>
<div class="section-title">付费抽奖档位概率T1-T5测试时使用</div>
<el-table
v-if="tierWeightsTableData.length"
:data="tierWeightsTableData"
v-if="paidTierTableData.length"
:data="paidTierTableData"
border
size="small"
class="tier-weights-table"
max-height="200"
max-height="160"
>
<el-table-column prop="tier" label="档位" width="80" align="center" />
<el-table-column prop="weight" label="权重" width="100" align="center" />
<el-table-column prop="percent" label="占比" width="100" align="center" />
</el-table>
<div v-else class="empty-tip">
暂无档位权重数据旧记录可能保存 tier_weights_snapshot
暂无付费档位数据旧记录可能保存 tier_weights_snapshot
</div>
</div>
<div class="detail-section">
<div class="section-title">免费抽奖档位概率T1-T5测试时使用</div>
<el-table
v-if="freeTierTableData.length"
:data="freeTierTableData"
border
size="small"
class="tier-weights-table"
max-height="160"
>
<el-table-column prop="tier" label="档位" width="80" align="center" />
<el-table-column prop="weight" label="权重" width="100" align="center" />
<el-table-column prop="percent" label="占比" width="100" align="center" />
</el-table>
<div v-else class="empty-tip">暂无免费档位数据</div>
</div>
<div class="detail-section">
<div class="section-title">权重配比快照测试时使用的 T1-T5/BIGWIN 配置</div>
<el-table
@@ -87,31 +107,50 @@
<!-- 导入弹窗 -->
<el-dialog
v-model="importVisible"
title="导入权重到正式配置"
width="460px"
title="导入到正式配置"
width="520px"
align-center
:close-on-click-modal="false"
>
<p class="import-desc">
将本测试记录权重配比快照写入 DiceRewardConfigT1-T5
档位权重写入所选奖池配置并刷新缓存
将本测试记录导入<strong>DiceReward</strong>格子权重
<strong>DiceRewardConfig</strong>BIGWIN weight
<strong>DiceLotteryPoolConfig</strong>付费/免费 T1-T5 档位概率请选择要写入的奖池
</p>
<el-form label-width="120px">
<el-form-item label="导入档位到奖池">
<el-form label-width="160px">
<el-form-item label="导入付费档位概率到奖池">
<el-select
v-model="importLotteryConfigId"
placeholder="选择要写入 T1-T5 权重的奖池配置"
v-model="importPaidLotteryConfigId"
placeholder="选择任意奖池(建议付费池)"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="opt in lotteryConfigOptions"
v-for="opt in paidLotteryOptions"
:key="opt.id"
:label="opt.name"
:value="opt.id"
/>
</el-select>
<div class="form-tip">不选则使用本记录保存时的奖池配置 ID</div>
<div class="form-tip">不选则使用本记录保存时的付费奖池配置 ID</div>
</el-form-item>
<el-form-item label="导入免费档位概率到奖池">
<el-select
v-model="importFreeLotteryConfigId"
placeholder="选择任意奖池(建议免费池)"
clearable
filterable
style="width: 100%"
>
<el-option
v-for="opt in freeLotteryOptions"
:key="opt.id"
:label="opt.name"
:value="opt.id"
/>
</el-select>
<div class="form-tip">不选则使用本记录保存时的免费奖池配置 ID</div>
</el-form-item>
</el-form>
<template #footer>
@@ -140,7 +179,18 @@
admin_id?: number | null
admin_name?: string
lottery_config_id?: number | null
tier_weights_snapshot?: Record<string, number>
paid_lottery_config_id?: number | null
free_lottery_config_id?: number | null
// 新结构:{ paid: {T1..T5}, free: {T1..T5} },兼容旧结构直接是 {T1..T5}
tier_weights_snapshot?:
| {
paid?: Record<string, number>
free?: Record<string, number>
[key: string]: any
}
| Record<string, number>
paid_tier_weights?: Record<string, number>
free_tier_weights?: Record<string, number>
weight_config_snapshot?: Array<{
id?: number
grid_number?: number
@@ -171,11 +221,11 @@
const importVisible = ref(false)
const importing = ref(false)
const importLotteryConfigId = ref<number | null>(null)
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
const importPaidLotteryConfigId = ref<number | null>(null)
const importFreeLotteryConfigId = ref<number | null>(null)
const lotteryConfigOptions = ref<Array<{ id: number; name: string; type: number }>>([])
const tierWeightsTableData = computed(() => {
const t = props.record?.tier_weights_snapshot
function tierWeightsToTableData(t: Record<string, number> | null | undefined) {
if (!t || typeof t !== 'object') return []
const tiers = ['T1', 'T2', 'T3', 'T4', 'T5']
const rows = tiers.map((tier) => {
@@ -188,8 +238,51 @@
weight: r.weight,
percent: total > 0 ? `${((r.weight / total) * 100).toFixed(1)}%` : '—'
}))
}
const paidTierTableData = computed(() => {
const r = props.record
const paidFromRecord = r?.paid_tier_weights
const snapshot = r?.tier_weights_snapshot
let snapshotPaid: Record<string, number> | null = null
if (snapshot && typeof snapshot === 'object') {
if ('paid' in snapshot || 'free' in snapshot) {
const s: any = snapshot
if (s.paid && typeof s.paid === 'object') {
snapshotPaid = s.paid
}
} else {
// 兼容旧结构:直接是 T1-T5
snapshotPaid = snapshot as Record<string, number>
}
}
const source =
paidFromRecord && Object.keys(paidFromRecord).length ? paidFromRecord : snapshotPaid
return tierWeightsToTableData(source || undefined)
})
const freeTierTableData = computed(() => {
const r = props.record
const freeFromRecord = r?.free_tier_weights
const snapshot = r?.tier_weights_snapshot
let snapshotFree: Record<string, number> | null = null
if (snapshot && typeof snapshot === 'object') {
if ('paid' in snapshot || 'free' in snapshot) {
const s: any = snapshot
if (s.free && typeof s.free === 'object') {
snapshotFree = s.free
}
}
}
const source =
freeFromRecord && Object.keys(freeFromRecord).length ? freeFromRecord : snapshotFree
return tierWeightsToTableData(source || undefined)
})
// 导入不限制奖池类型,两个下拉都可选任意 DiceLotteryPoolConfig
const paidLotteryOptions = computed(() => lotteryConfigOptions.value)
const freeLotteryOptions = computed(() => lotteryConfigOptions.value)
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
@@ -245,7 +338,9 @@
}
function openImport() {
importLotteryConfigId.value = props.record?.lottery_config_id ?? null
importPaidLotteryConfigId.value =
props.record?.paid_lottery_config_id ?? props.record?.lottery_config_id ?? null
importFreeLotteryConfigId.value = props.record?.free_lottery_config_id ?? null
importVisible.value = true
loadLotteryOptions()
}
@@ -257,9 +352,10 @@
try {
await recordApi.importFromRecord({
record_id: id,
lottery_config_id: importLotteryConfigId.value ?? undefined
paid_lottery_config_id: importPaidLotteryConfigId.value ?? undefined,
free_lottery_config_id: importFreeLotteryConfigId.value ?? undefined
})
ElMessage.success('导入成功,已刷新奖励配置与奖池配置')
ElMessage.success('导入成功,已刷新 DiceReward、DiceRewardConfig(BIGWIN)、奖池配置')
importVisible.value = false
emit('import-done')
} catch (e: any) {