优化一键测试权重
This commit is contained in:
@@ -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_weight~t5_weight,用于一键测试权重档位类型下拉
|
||||
* type:0=付费抽奖券,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),
|
||||
|
||||
@@ -57,9 +57,22 @@ export default {
|
||||
},
|
||||
|
||||
/**
|
||||
* 一键测试权重:创建测试记录并启动后台执行,返回 record_id 用于轮询进度
|
||||
* 一键测试权重:创建测试记录并启动后台执行,按付费/免费、顺逆方向交替抽奖
|
||||
* 可选 lottery_config_id;不选则传 paid_tier_weights / free_tier_weights(T1-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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">自定义档位概率(T1~T5),每档 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">自定义档位概率(T1~T5),每档 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=1(DiceLotteryPoolConfig.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('付费未选奖池时,T1~T5 档位概率之和需大于 0')
|
||||
return false
|
||||
}
|
||||
if (sum > 100) {
|
||||
ElMessage.warning('付费档位概率 T1~T5 之和不能超过 100%')
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (needFreeTier) {
|
||||
const sum = freeTierSum.value
|
||||
if (sum <= 0) {
|
||||
ElMessage.warning('免费未选奖池时,T1~T5 档位概率之和需大于 0')
|
||||
return false
|
||||
}
|
||||
if (sum > 100) {
|
||||
ElMessage.warning('免费档位概率 T1~T5 之和不能超过 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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
将本测试记录的「权重配比快照」写入 DiceRewardConfig,将「T1-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) {
|
||||
|
||||
@@ -430,11 +430,13 @@ class PlayStartLogic
|
||||
|
||||
/**
|
||||
* 模拟一局抽奖(不写库、不扣玩家),用于权重测试写入 dice_play_record_test
|
||||
* @param \app\dice\model\lottery_pool_config\DiceLotteryPoolConfig $config 奖池配置
|
||||
* @param \app\dice\model\lottery_pool_config\DiceLotteryPoolConfig|null $config 奖池配置,自定义档位时可为 null
|
||||
* @param int $direction 0=顺时针 1=逆时针
|
||||
* @param int $lotteryType 0=付费 1=免费
|
||||
* @param array|null $customTierWeights 自定义档位权重 ['T1'=>x, 'T2'=>x, ...],非空时忽略 config 的档位权重
|
||||
* @return array 可直接用于 DicePlayRecordTest::create 的字段 + tier(用于统计档位概率)
|
||||
*/
|
||||
public function simulateOnePlay($config, int $direction): array
|
||||
public function simulateOnePlay($config, int $direction, int $lotteryType = 0, ?array $customTierWeights = null): array
|
||||
{
|
||||
$rewardInstance = DiceReward::getCachedInstance();
|
||||
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
|
||||
@@ -442,7 +444,14 @@ class PlayStartLogic
|
||||
$chosen = null;
|
||||
$tier = null;
|
||||
for ($tierAttempt = 0; $tierAttempt < $maxTierRetry; $tierAttempt++) {
|
||||
$tier = LotteryService::drawTierByWeights($config);
|
||||
if ($customTierWeights !== null && $customTierWeights !== []) {
|
||||
$tier = LotteryService::drawTierByWeightsFromArray($customTierWeights);
|
||||
} else {
|
||||
if ($config === null) {
|
||||
throw new \RuntimeException('模拟抽奖:未提供奖池配置或自定义档位权重');
|
||||
}
|
||||
$tier = LotteryService::drawTierByWeights($config);
|
||||
}
|
||||
$tierRewards = $byTierDirection[$tier][$direction] ?? [];
|
||||
if (empty($tierRewards)) {
|
||||
continue;
|
||||
@@ -495,15 +504,15 @@ class PlayStartLogic
|
||||
}
|
||||
|
||||
$winCoin = $superWinCoin + $rewardWinCoin;
|
||||
$configId = (int) $config->id;
|
||||
$configId = $config !== null ? (int) $config->id : 0;
|
||||
$rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex;
|
||||
$configName = (string) ($config->name ?? '');
|
||||
$configName = $config !== null ? (string) ($config->name ?? '') : '自定义';
|
||||
|
||||
return [
|
||||
'player_id' => 0,
|
||||
'admin_id' => 0,
|
||||
'lottery_config_id' => $configId,
|
||||
'lottery_type' => self::LOTTERY_TYPE_PAID,
|
||||
'lottery_type' => $lotteryType,
|
||||
'is_win' => $isWin,
|
||||
'win_coin' => $winCoin,
|
||||
'super_win_coin' => $superWinCoin,
|
||||
|
||||
@@ -114,6 +114,20 @@ class LotteryService
|
||||
return self::drawTierByWeightArray($tiers, $weights);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 T1-T5 权重数组抽取档位(用于测试自定义档位概率)
|
||||
* @param array $tierWeights 如 ['T1'=>100, 'T2'=>200, ...] 或 [100,200,300,400,500]
|
||||
*/
|
||||
public static function drawTierByWeightsFromArray(array $tierWeights): string
|
||||
{
|
||||
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||
$weights = [];
|
||||
foreach ($tiers as $i => $t) {
|
||||
$weights[] = (int) ($tierWeights[$t] ?? $tierWeights[$i] ?? 0);
|
||||
}
|
||||
return self::drawTierByWeightArray($tiers, $weights);
|
||||
}
|
||||
|
||||
/** 按档位权重数组抽取 T1-T5 */
|
||||
private static function drawTierByWeightArray(array $tiers, array $weights): string
|
||||
{
|
||||
|
||||
@@ -30,20 +30,22 @@ class DiceLotteryPoolConfigController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 DiceLotteryPoolConfig 列表数据,用于 lottery_config_id 下拉(值为 id,显示为 name),并附带 T1-T5 档位权重
|
||||
* 获取 DiceLotteryPoolConfig 列表数据,用于 lottery_config_id 下拉(值为 id,显示为 name),并附带 type、T1-T5 档位权重
|
||||
* type:0=付费抽奖券,1=免费抽奖券;一键测试权重中付费默认选 type=0,免费默认选 type=1
|
||||
* @param Request $request
|
||||
* @return Response 返回 [ ['id' => int, 'name' => string, 't1_weight' => int, ... 't5_weight' => int], ... ]
|
||||
* @return Response 返回 [ ['id' => int, 'name' => string, 'type' => int, 't1_weight' => int, ... 't5_weight' => int], ... ]
|
||||
*/
|
||||
#[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
|
||||
public function getOptions(Request $request): Response
|
||||
{
|
||||
$list = DiceLotteryPoolConfig::field('id,name,t1_weight,t2_weight,t3_weight,t4_weight,t5_weight')
|
||||
$list = DiceLotteryPoolConfig::field('id,name,type,t1_weight,t2_weight,t3_weight,t4_weight,t5_weight')
|
||||
->order('id', 'asc')
|
||||
->select();
|
||||
$data = $list->map(function ($item) {
|
||||
return [
|
||||
'id' => (int) $item['id'],
|
||||
'name' => (string) ($item['name'] ?? ''),
|
||||
'type' => (int) ($item['type'] ?? 0),
|
||||
't1_weight' => (int) ($item['t1_weight'] ?? 0),
|
||||
't2_weight' => (int) ($item['t2_weight'] ?? 0),
|
||||
't3_weight' => (int) ($item['t3_weight'] ?? 0),
|
||||
|
||||
@@ -30,7 +30,7 @@ class DicePlayRecordTestController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据列表,并在结果中附带当前筛选条件下所有测试数据的玩家总收益 total_win_coin(DicePlayRecordTest.win_coin 求和)
|
||||
* 数据列表,并在结果中附带当前筛选条件下测试数据的平台总盈利 total_win_coin(付费抽奖次数×100 - 玩家总收益)
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
@@ -44,13 +44,17 @@ class DicePlayRecordTestController extends BaseController
|
||||
['win_coin_min', ''],
|
||||
['win_coin_max', ''],
|
||||
['reward_tier', ''],
|
||||
['roll_number', ''],
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
$query->with(['diceLotteryPoolConfig', 'diceRewardConfig']);
|
||||
|
||||
// 按当前筛选条件统计所有测试数据的总收益(游戏总亏损)
|
||||
// 按当前筛选条件统计:平台总盈利 = 付费抽奖(lottery_type=0)次数×100 - 玩家总收益(win_coin 求和)
|
||||
$sumQuery = clone $query;
|
||||
$totalWinCoin = $sumQuery->sum('win_coin');
|
||||
$playerTotalWin = (float) $sumQuery->sum('win_coin');
|
||||
$paidCountQuery = clone $query;
|
||||
$paidCount = (int) $paidCountQuery->where('lottery_type', 0)->count();
|
||||
$totalWinCoin = $paidCount * 100 - $playerTotalWin;
|
||||
|
||||
$data = $this->logic->getList($query);
|
||||
$data['total_win_coin'] = $totalWinCoin;
|
||||
|
||||
@@ -79,20 +79,31 @@ class DiceRewardController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 一键测试权重:创建测试记录并启动单进程后台执行,实时写入 dice_play_record_test,更新 dice_reward_config_record 进度
|
||||
* 参数:lottery_config_id 奖池配置,s_count 顺时针次数 100/500/1000/5000,n_count 逆时针次数 100/500/1000/5000
|
||||
* 一键测试权重:创建测试记录并启动单进程后台执行,按付费/免费、顺逆方向交替写入 dice_play_record_test
|
||||
* 参数:lottery_config_id 可选,不选则传 paid_tier_weights / free_tier_weights 自定义档位;
|
||||
* paid_s_count, paid_n_count, free_s_count, free_n_count;或兼容旧版 s_count, n_count
|
||||
*/
|
||||
#[Permission('奖励对照列表', 'dice:reward:index:index')]
|
||||
public function startWeightTest(Request $request): Response
|
||||
{
|
||||
$lotteryConfigId = (int) $request->post('lottery_config_id', 0);
|
||||
$sCount = (int) $request->post('s_count', 100);
|
||||
$nCount = (int) $request->post('n_count', 100);
|
||||
$post = is_array($request->post()) ? $request->post() : [];
|
||||
$params = [
|
||||
'lottery_config_id' => $post['lottery_config_id'] ?? null,
|
||||
'paid_lottery_config_id' => $post['paid_lottery_config_id'] ?? null,
|
||||
'free_lottery_config_id' => $post['free_lottery_config_id'] ?? null,
|
||||
's_count' => $post['s_count'] ?? null,
|
||||
'n_count' => $post['n_count'] ?? null,
|
||||
'paid_s_count' => $post['paid_s_count'] ?? null,
|
||||
'paid_n_count' => $post['paid_n_count'] ?? null,
|
||||
'free_s_count' => $post['free_s_count'] ?? null,
|
||||
'free_n_count' => $post['free_n_count'] ?? null,
|
||||
'paid_tier_weights' => $post['paid_tier_weights'] ?? null,
|
||||
'free_tier_weights' => $post['free_tier_weights'] ?? null,
|
||||
];
|
||||
$adminId = isset($this->adminInfo['id']) ? (int) $this->adminInfo['id'] : null;
|
||||
try {
|
||||
$logic = new DiceRewardConfigRecordLogic();
|
||||
$recordId = $logic->createWeightTestRecord($lotteryConfigId, $sCount, $nCount, $adminId);
|
||||
// 由独立进程 WeightTestProcess 定时轮询 status=0 并执行,不占用 HTTP 资源
|
||||
$recordId = $logic->createWeightTestRecord($params, $adminId);
|
||||
return $this->success(['record_id' => $recordId]);
|
||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
|
||||
@@ -141,26 +141,25 @@ class DiceRewardConfigRecordController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入:将测试记录的权重写入 DiceRewardConfig 与 DiceLotteryPoolConfig,并重新实例化缓存
|
||||
* @param Request $request record_id: 测试记录ID, lottery_config_id: 可选,导入档位权重到的奖池配置ID,不传则用记录内的 lottery_config_id
|
||||
* @return Response
|
||||
* 导入:测试记录 → DiceReward、DiceRewardConfig(BIGWIN)、DiceLotteryPoolConfig(付费/免费 T1-T5)
|
||||
* @param Request $request record_id, paid_lottery_config_id(可选), free_lottery_config_id(可选), lottery_config_id(兼容旧版)
|
||||
*/
|
||||
#[Permission('奖励配置权重测试记录列表', 'dice:reward_config_record:index:index')]
|
||||
public function importFromRecord(Request $request): Response
|
||||
{
|
||||
$recordId = (int) $request->post('record_id', 0);
|
||||
$lotteryConfigId = $request->post('lottery_config_id', null);
|
||||
if ($recordId <= 0) {
|
||||
return $this->fail('请指定测试记录');
|
||||
}
|
||||
if ($lotteryConfigId !== null && $lotteryConfigId !== '') {
|
||||
$lotteryConfigId = (int) $lotteryConfigId;
|
||||
} else {
|
||||
$lotteryConfigId = null;
|
||||
}
|
||||
$paidId = $request->post('paid_lottery_config_id', null);
|
||||
$freeId = $request->post('free_lottery_config_id', null);
|
||||
$legacyId = $request->post('lottery_config_id', null);
|
||||
$paidLotteryConfigId = $paidId !== null && $paidId !== '' ? (int) $paidId : null;
|
||||
$freeLotteryConfigId = $freeId !== null && $freeId !== '' ? (int) $freeId : null;
|
||||
$lotteryConfigId = $legacyId !== null && $legacyId !== '' ? (int) $legacyId : null;
|
||||
try {
|
||||
$this->logic->importFromRecord($recordId, $lotteryConfigId);
|
||||
return $this->success('导入成功,已刷新奖励配置与奖池配置');
|
||||
$this->logic->importFromRecord($recordId, $paidLotteryConfigId, $freeLotteryConfigId, $lotteryConfigId);
|
||||
return $this->success('导入成功,已刷新 DiceReward、DiceRewardConfig(BIGWIN)、奖池配置');
|
||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
|
||||
@@ -71,12 +71,13 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 将测试记录的权重导入:weight_config_snapshot → dice_reward(顺时针/逆时针同值),tier_weights_snapshot → DiceLotteryPoolConfig,并刷新缓存
|
||||
* 将测试记录导入:DiceReward(权重快照)、DiceRewardConfig(BIGWIN weight)、DiceLotteryPoolConfig(付费/免费 T1-T5)
|
||||
* @param int $recordId 测试记录 ID
|
||||
* @param int|null $lotteryConfigId 要导入档位权重的奖池配置 ID;不传则使用记录中的 lottery_config_id(若有)
|
||||
* @throws ApiException
|
||||
* @param int|null $paidLotteryConfigId 导入付费档位概率到的奖池(type=0),不传则用记录 paid_lottery_config_id
|
||||
* @param int|null $freeLotteryConfigId 导入免费档位概率到的奖池(type=1),不传则用记录 free_lottery_config_id
|
||||
* @param int|null $lotteryConfigId 兼容旧版:不传 paid/free 时用作统一奖池
|
||||
*/
|
||||
public function importFromRecord(int $recordId, ?int $lotteryConfigId = null): void
|
||||
public function importFromRecord(int $recordId, ?int $paidLotteryConfigId = null, ?int $freeLotteryConfigId = null, ?int $lotteryConfigId = null): void
|
||||
{
|
||||
$record = $this->model->find($recordId);
|
||||
if (!$record) {
|
||||
@@ -96,11 +97,15 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
if ($id <= 0) {
|
||||
continue;
|
||||
}
|
||||
$tier = DiceRewardConfig::where('id', $id)->value('tier');
|
||||
if ($tier === null || $tier === '') {
|
||||
$tier = isset($item['tier']) ? (string) $item['tier'] : '';
|
||||
if ($tier === '') {
|
||||
$tier = DiceRewardConfig::where('id', $id)->value('tier');
|
||||
$tier = $tier !== null && $tier !== '' ? (string) $tier : '';
|
||||
}
|
||||
if ($tier === '') {
|
||||
continue;
|
||||
}
|
||||
$tier = (string) $tier;
|
||||
// 写入 DiceReward(顺/逆时针同值)
|
||||
foreach ([DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE] as $direction) {
|
||||
$affected = DiceReward::where('tier', $tier)->where('direction', $direction)->where('end_index', $id)->update(['weight' => $weight]);
|
||||
if ($affected === 0) {
|
||||
@@ -112,30 +117,78 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
$m->save();
|
||||
}
|
||||
}
|
||||
// BIGWIN:同步写入 DiceRewardConfig.weight
|
||||
if (strtoupper($tier) === 'BIGWIN') {
|
||||
DiceRewardConfig::where('id', $id)->update(['weight' => $weight]);
|
||||
}
|
||||
}
|
||||
DiceReward::refreshCache();
|
||||
}
|
||||
|
||||
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||
$tierSnapshot = $record['tier_weights_snapshot'] ?? null;
|
||||
if (is_string($tierSnapshot)) {
|
||||
$tierSnapshot = json_decode($tierSnapshot, true);
|
||||
}
|
||||
$targetLotteryId = $lotteryConfigId !== null && $lotteryConfigId > 0
|
||||
? $lotteryConfigId
|
||||
: (isset($record['lottery_config_id']) && (int) $record['lottery_config_id'] > 0 ? (int) $record['lottery_config_id'] : null);
|
||||
if (is_array($tierSnapshot) && !empty($tierSnapshot) && $targetLotteryId > 0) {
|
||||
$pool = DiceLotteryPoolConfig::find($targetLotteryId);
|
||||
$paidWeights = $record['paid_tier_weights'] ?? null;
|
||||
if (is_string($paidWeights)) {
|
||||
$paidWeights = json_decode($paidWeights, true);
|
||||
}
|
||||
$freeWeights = $record['free_tier_weights'] ?? null;
|
||||
if (is_string($freeWeights)) {
|
||||
$freeWeights = json_decode($freeWeights, true);
|
||||
}
|
||||
$fallbackLotteryId = $lotteryConfigId > 0 ? $lotteryConfigId : (isset($record['lottery_config_id']) && (int) $record['lottery_config_id'] > 0 ? (int) $record['lottery_config_id'] : null);
|
||||
$paidTargetId = $paidLotteryConfigId > 0 ? $paidLotteryConfigId : ($fallbackLotteryId ?? (isset($record['paid_lottery_config_id']) && (int) $record['paid_lottery_config_id'] > 0 ? (int) $record['paid_lottery_config_id'] : null));
|
||||
$freeTargetId = $freeLotteryConfigId > 0 ? $freeLotteryConfigId : (isset($record['free_lottery_config_id']) && (int) $record['free_lottery_config_id'] > 0 ? (int) $record['free_lottery_config_id'] : null);
|
||||
|
||||
// tier_weights_snapshot 新结构:['paid' => [...], 'free' => [...]]
|
||||
$snapshotPaid = null;
|
||||
$snapshotFree = null;
|
||||
if (is_array($tierSnapshot) && !empty($tierSnapshot)) {
|
||||
if (array_key_exists('paid', $tierSnapshot) || array_key_exists('free', $tierSnapshot)) {
|
||||
if (isset($tierSnapshot['paid']) && is_array($tierSnapshot['paid'])) {
|
||||
$snapshotPaid = $tierSnapshot['paid'];
|
||||
}
|
||||
if (isset($tierSnapshot['free']) && is_array($tierSnapshot['free'])) {
|
||||
$snapshotFree = $tierSnapshot['free'];
|
||||
}
|
||||
} else {
|
||||
// 兼容旧结构:直接就是一个 T1-T5 的数组,视为付费
|
||||
$snapshotPaid = $tierSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
$paidData = is_array($paidWeights) && !empty($paidWeights) ? $paidWeights : $snapshotPaid;
|
||||
$freeData = is_array($freeWeights) && !empty($freeWeights) ? $freeWeights : $snapshotFree;
|
||||
|
||||
if (is_array($paidData) && $paidTargetId > 0) {
|
||||
$pool = DiceLotteryPoolConfig::find($paidTargetId);
|
||||
if (!$pool) {
|
||||
throw new ApiException('奖池配置不存在');
|
||||
throw new ApiException('付费奖池配置不存在');
|
||||
}
|
||||
$update = [
|
||||
't1_weight' => (int) ($tierSnapshot['T1'] ?? $tierSnapshot['t1'] ?? 0),
|
||||
't2_weight' => (int) ($tierSnapshot['T2'] ?? $tierSnapshot['t2'] ?? 0),
|
||||
't3_weight' => (int) ($tierSnapshot['T3'] ?? $tierSnapshot['t3'] ?? 0),
|
||||
't4_weight' => (int) ($tierSnapshot['T4'] ?? $tierSnapshot['t4'] ?? 0),
|
||||
't5_weight' => (int) ($tierSnapshot['T5'] ?? $tierSnapshot['t5'] ?? 0),
|
||||
't1_weight' => (int) ($paidData['T1'] ?? $paidData['t1'] ?? 0),
|
||||
't2_weight' => (int) ($paidData['T2'] ?? $paidData['t2'] ?? 0),
|
||||
't3_weight' => (int) ($paidData['T3'] ?? $paidData['t3'] ?? 0),
|
||||
't4_weight' => (int) ($paidData['T4'] ?? $paidData['t4'] ?? 0),
|
||||
't5_weight' => (int) ($paidData['T5'] ?? $paidData['t5'] ?? 0),
|
||||
];
|
||||
DiceLotteryPoolConfig::where('id', $targetLotteryId)->update($update);
|
||||
DiceLotteryPoolConfig::where('id', $paidTargetId)->update($update);
|
||||
}
|
||||
if (is_array($freeData) && $freeTargetId > 0) {
|
||||
$pool = DiceLotteryPoolConfig::find($freeTargetId);
|
||||
if (!$pool) {
|
||||
throw new ApiException('免费奖池配置不存在');
|
||||
}
|
||||
$update = [
|
||||
't1_weight' => (int) ($freeData['T1'] ?? $freeData['t1'] ?? 0),
|
||||
't2_weight' => (int) ($freeData['T2'] ?? $freeData['t2'] ?? 0),
|
||||
't3_weight' => (int) ($freeData['T3'] ?? $freeData['t3'] ?? 0),
|
||||
't4_weight' => (int) ($freeData['T4'] ?? $freeData['t4'] ?? 0),
|
||||
't5_weight' => (int) ($freeData['T5'] ?? $freeData['t5'] ?? 0),
|
||||
];
|
||||
DiceLotteryPoolConfig::where('id', $freeTargetId)->update($update);
|
||||
}
|
||||
|
||||
DiceRewardConfig::refreshCache();
|
||||
@@ -143,28 +196,63 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一键测试权重记录并返回 ID,供后台执行器写入 dice_play_record_test 并更新进度
|
||||
* @param int $lotteryConfigId 奖池配置 ID(DiceLotteryPoolConfig)
|
||||
* @param int $sCount 顺时针模拟次数 100/500/1000/5000
|
||||
* @param int $nCount 逆时针模拟次数 100/500/1000/5000
|
||||
* @param int|null $adminId 执行人
|
||||
* 创建一键测试权重记录并返回 ID,供后台执行器按付费/免费、顺逆方向交替写入 dice_play_record_test
|
||||
* 支持两种模式:1)选择奖池配置 lottery_config_id,档位概率取自配置;2)不选配置,使用自定义 paid_tier_weights / free_tier_weights
|
||||
* @param array|int $params 数组:lottery_config_id(可选), paid_s_count, paid_n_count, free_s_count, free_n_count;或兼容旧版传 4 个 int 时视为 (paid_s_count, paid_n_count, free_s_count, free_n_count)
|
||||
* @param int|null $adminId 执行人(旧版 4 参调用时第二参为 paid_n_count,此处不传 adminId)
|
||||
* @return int 记录 ID
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function createWeightTestRecord(int $lotteryConfigId, int $sCount, int $nCount, ?int $adminId = null): int
|
||||
public function createWeightTestRecord(array|int $params, mixed $adminIdOrFreeS = null, mixed $freeSOrFreeN = null, mixed $freeN = null): int
|
||||
{
|
||||
$adminId = null;
|
||||
if (!is_array($params)) {
|
||||
// 兼容旧版调用:createWeightTestRecord(paid_s_count, paid_n_count, free_s_count, free_n_count)
|
||||
$params = [
|
||||
'paid_s_count' => (int) $params,
|
||||
'paid_n_count' => (int) $adminIdOrFreeS,
|
||||
'free_s_count' => (int) $freeSOrFreeN,
|
||||
'free_n_count' => (int) $freeN,
|
||||
];
|
||||
} else {
|
||||
$adminId = $adminIdOrFreeS !== null && $adminIdOrFreeS !== '' ? (int) $adminIdOrFreeS : null;
|
||||
}
|
||||
$allowed = [100, 500, 1000, 5000];
|
||||
if (!in_array($sCount, $allowed, true) || !in_array($nCount, $allowed, true)) {
|
||||
throw new ApiException('顺时针/逆时针次数仅支持 100、500、1000、5000');
|
||||
$lotteryConfigId = isset($params['lottery_config_id']) ? (int) $params['lottery_config_id'] : 0;
|
||||
$paidConfigId = isset($params['paid_lottery_config_id']) ? (int) $params['paid_lottery_config_id'] : 0;
|
||||
$freeConfigId = isset($params['free_lottery_config_id']) ? (int) $params['free_lottery_config_id'] : 0;
|
||||
if ($paidConfigId <= 0 && $lotteryConfigId > 0) {
|
||||
$paidConfigId = $lotteryConfigId;
|
||||
}
|
||||
$config = DiceLotteryPoolConfig::find($lotteryConfigId);
|
||||
if (!$config) {
|
||||
throw new ApiException('奖池配置不存在');
|
||||
if ($freeConfigId <= 0 && $lotteryConfigId > 0) {
|
||||
$freeConfigId = $lotteryConfigId;
|
||||
}
|
||||
$paidS = isset($params['paid_s_count']) ? (int) $params['paid_s_count'] : (int) ($params['s_count'] ?? 0);
|
||||
$paidN = isset($params['paid_n_count']) ? (int) $params['paid_n_count'] : (int) ($params['n_count'] ?? 0);
|
||||
$freeS = (int) ($params['free_s_count'] ?? 0);
|
||||
$freeN = (int) ($params['free_n_count'] ?? 0);
|
||||
|
||||
foreach ([$paidS, $paidN, $freeS, $freeN] as $c) {
|
||||
if ($c !== 0 && !in_array($c, $allowed, true)) {
|
||||
throw new ApiException('各抽奖次数仅支持 0、100、500、1000、5000');
|
||||
}
|
||||
}
|
||||
$total = $paidS + $paidN + $freeS + $freeN;
|
||||
if ($total <= 0) {
|
||||
throw new ApiException('付费或免费至少一种方向次数之和大于 0');
|
||||
}
|
||||
|
||||
$snapshot = [];
|
||||
// 档位权重快照:区分付费/免费,结构为 ['paid' => [...], 'free' => [...]]
|
||||
$tierWeightsSnapshot = [
|
||||
'paid' => null,
|
||||
'free' => null,
|
||||
];
|
||||
$paidTierWeights = null;
|
||||
$freeTierWeights = null;
|
||||
|
||||
$instance = DiceReward::getCachedInstance();
|
||||
$byTierDirection = $instance['by_tier_direction'] ?? [];
|
||||
$snapshot = [];
|
||||
foreach ($byTierDirection as $tier => $byDir) {
|
||||
foreach ($byDir as $dir => $rows) {
|
||||
foreach ($rows as $row) {
|
||||
@@ -177,26 +265,98 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
}
|
||||
}
|
||||
}
|
||||
$tierWeightsSnapshot = [
|
||||
'T1' => (int) ($config->t1_weight ?? 0),
|
||||
'T2' => (int) ($config->t2_weight ?? 0),
|
||||
'T3' => (int) ($config->t3_weight ?? 0),
|
||||
'T4' => (int) ($config->t4_weight ?? 0),
|
||||
'T5' => (int) ($config->t5_weight ?? 0),
|
||||
];
|
||||
$total = $sCount + $nCount;
|
||||
|
||||
if ($paidConfigId > 0) {
|
||||
$config = DiceLotteryPoolConfig::find($paidConfigId);
|
||||
if (!$config) {
|
||||
throw new ApiException('付费奖池配置不存在');
|
||||
}
|
||||
$tierWeightsSnapshot['paid'] = [
|
||||
'T1' => (int) ($config->t1_weight ?? 0),
|
||||
'T2' => (int) ($config->t2_weight ?? 0),
|
||||
'T3' => (int) ($config->t3_weight ?? 0),
|
||||
'T4' => (int) ($config->t4_weight ?? 0),
|
||||
'T5' => (int) ($config->t5_weight ?? 0),
|
||||
];
|
||||
} else {
|
||||
$paidTierWeights = $params['paid_tier_weights'] ?? null;
|
||||
if (!is_array($paidTierWeights)) {
|
||||
throw new ApiException('付费未选择奖池配置时,请填写付费自定义档位概率(T1~T5)');
|
||||
}
|
||||
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||
foreach ($tiers as $t) {
|
||||
$v = (int) ($paidTierWeights[$t] ?? 0);
|
||||
if ($v < 0 || $v > 100) {
|
||||
throw new ApiException('付费档位概率每档只能 0-100%');
|
||||
}
|
||||
$paidTierWeights[$t] = $v;
|
||||
}
|
||||
$paidSum = array_sum(array_intersect_key($paidTierWeights, array_flip($tiers)));
|
||||
if ($paidSum > 100) {
|
||||
throw new ApiException('付费档位概率 T1~T5 之和不能超过 100%');
|
||||
}
|
||||
$tierWeightsSnapshot['paid'] = $paidTierWeights;
|
||||
}
|
||||
|
||||
if ($freeConfigId > 0) {
|
||||
$config = DiceLotteryPoolConfig::find($freeConfigId);
|
||||
if (!$config) {
|
||||
throw new ApiException('免费奖池配置不存在');
|
||||
}
|
||||
$tierWeightsSnapshot['free'] = [
|
||||
'T1' => (int) ($config->t1_weight ?? 0),
|
||||
'T2' => (int) ($config->t2_weight ?? 0),
|
||||
'T3' => (int) ($config->t3_weight ?? 0),
|
||||
'T4' => (int) ($config->t4_weight ?? 0),
|
||||
'T5' => (int) ($config->t5_weight ?? 0),
|
||||
];
|
||||
} else {
|
||||
$freeTierWeights = $params['free_tier_weights'] ?? null;
|
||||
if (!is_array($freeTierWeights)) {
|
||||
throw new ApiException('免费未选择奖池配置时,请填写免费自定义档位概率(T1~T5)');
|
||||
}
|
||||
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||
foreach ($tiers as $t) {
|
||||
$v = (int) ($freeTierWeights[$t] ?? 0);
|
||||
if ($v < 0 || $v > 100) {
|
||||
throw new ApiException('免费档位概率每档只能 0-100%');
|
||||
}
|
||||
$freeTierWeights[$t] = $v;
|
||||
}
|
||||
$freeSum = array_sum(array_intersect_key($freeTierWeights, array_flip($tiers)));
|
||||
if ($freeSum > 100) {
|
||||
throw new ApiException('免费档位概率 T1~T5 之和不能超过 100%');
|
||||
}
|
||||
$tierWeightsSnapshot['free'] = $freeTierWeights;
|
||||
}
|
||||
|
||||
// 兼容:若某一侧未配置,则保存为空数组,方便前端直接解构
|
||||
if (!is_array($tierWeightsSnapshot['paid'])) {
|
||||
$tierWeightsSnapshot['paid'] = [];
|
||||
}
|
||||
if (!is_array($tierWeightsSnapshot['free'])) {
|
||||
$tierWeightsSnapshot['free'] = [];
|
||||
}
|
||||
|
||||
$record = new DiceRewardConfigRecord();
|
||||
$record->test_count = $total;
|
||||
$record->weight_config_snapshot = $snapshot;
|
||||
$record->tier_weights_snapshot = $tierWeightsSnapshot;
|
||||
$record->lottery_config_id = $lotteryConfigId;
|
||||
$record->lottery_config_id = $lotteryConfigId > 0 ? $lotteryConfigId : null;
|
||||
$record->paid_lottery_config_id = $paidConfigId > 0 ? $paidConfigId : null;
|
||||
$record->free_lottery_config_id = $freeConfigId > 0 ? $freeConfigId : null;
|
||||
$record->total_play_count = $total;
|
||||
$record->over_play_count = 0;
|
||||
$record->status = DiceRewardConfigRecord::STATUS_RUNNING;
|
||||
$record->remark = null;
|
||||
$record->s_count = $sCount;
|
||||
$record->n_count = $nCount;
|
||||
$record->s_count = $paidS + $paidN;
|
||||
$record->n_count = $freeS + $freeN;
|
||||
$record->paid_s_count = $paidS;
|
||||
$record->paid_n_count = $paidN;
|
||||
$record->free_s_count = $freeS;
|
||||
$record->free_n_count = $freeN;
|
||||
$record->paid_tier_weights = $paidTierWeights;
|
||||
$record->free_tier_weights = $freeTierWeights;
|
||||
$record->result_counts = [];
|
||||
$record->tier_counts = null;
|
||||
$record->admin_id = $adminId;
|
||||
|
||||
@@ -17,7 +17,8 @@ class WeightTestRunner
|
||||
private const BATCH_SIZE = 10;
|
||||
|
||||
/**
|
||||
* 执行指定测试记录:按 s_count(顺时针)+n_count(逆时针) 模拟,每 10 条写入一次测试表并更新进度
|
||||
* 执行指定测试记录:按付费/免费、顺/逆方向交替模拟(付费顺→付费逆→免费顺→免费逆),每 10 条写入一次测试表并更新进度
|
||||
* 支持:1)lottery_config_id 有值时用奖池配置档位权重;2)无值时用记录中的 paid_tier_weights / free_tier_weights
|
||||
* @param int $recordId dice_reward_config_record.id
|
||||
*/
|
||||
public function run(int $recordId): void
|
||||
@@ -28,39 +29,91 @@ class WeightTestRunner
|
||||
return;
|
||||
}
|
||||
|
||||
$sCount = (int) ($record->s_count ?? 0);
|
||||
$nCount = (int) ($record->n_count ?? 0);
|
||||
$total = $sCount + $nCount;
|
||||
if ($total <= 0) {
|
||||
$this->markFailed($recordId, 's_count + n_count 必须大于 0');
|
||||
return;
|
||||
$paidS = (int) ($record->paid_s_count ?? 0);
|
||||
$paidN = (int) ($record->paid_n_count ?? 0);
|
||||
$freeS = (int) ($record->free_s_count ?? 0);
|
||||
$freeN = (int) ($record->free_n_count ?? 0);
|
||||
if ($paidS + $paidN + $freeS + $freeN <= 0) {
|
||||
$sCount = (int) ($record->s_count ?? 0);
|
||||
$nCount = (int) ($record->n_count ?? 0);
|
||||
$total = $sCount + $nCount;
|
||||
if ($total <= 0) {
|
||||
$this->markFailed($recordId, '抽奖次数必须大于 0');
|
||||
return;
|
||||
}
|
||||
$paidS = $sCount;
|
||||
$paidN = $nCount;
|
||||
} else {
|
||||
$total = $paidS + $paidN + $freeS + $freeN;
|
||||
}
|
||||
|
||||
$configId = (int) ($record->lottery_config_id ?? 0);
|
||||
$config = $configId > 0 ? DiceLotteryPoolConfig::find($configId) : DiceLotteryPoolConfig::where('type', 0)->find();
|
||||
if (!$config) {
|
||||
$this->markFailed($recordId, '奖池配置不存在');
|
||||
$paidConfigId = (int) ($record->paid_lottery_config_id ?? 0);
|
||||
$freeConfigId = (int) ($record->free_lottery_config_id ?? 0);
|
||||
if ($paidConfigId <= 0) {
|
||||
$paidConfigId = (int) ($record->lottery_config_id ?? 0);
|
||||
}
|
||||
if ($freeConfigId <= 0) {
|
||||
$freeConfigId = (int) ($record->lottery_config_id ?? 0);
|
||||
}
|
||||
$paidConfig = $paidConfigId > 0 ? DiceLotteryPoolConfig::find($paidConfigId) : null;
|
||||
$freeConfig = $freeConfigId > 0 ? DiceLotteryPoolConfig::find($freeConfigId) : null;
|
||||
if ($paidConfigId > 0 && !$paidConfig) {
|
||||
$this->markFailed($recordId, '付费奖池配置不存在');
|
||||
return;
|
||||
}
|
||||
if ($freeConfigId > 0 && !$freeConfig) {
|
||||
$this->markFailed($recordId, '免费奖池配置不存在');
|
||||
return;
|
||||
}
|
||||
$paidTierWeights = null;
|
||||
$freeTierWeights = null;
|
||||
if ($paidConfig === null) {
|
||||
$paidTierWeights = $record->paid_tier_weights;
|
||||
if (!is_array($paidTierWeights) || $paidTierWeights === []) {
|
||||
$this->markFailed($recordId, '付费未选奖池时需提供 paid_tier_weights');
|
||||
return;
|
||||
}
|
||||
}
|
||||
if ($freeConfig === null) {
|
||||
$freeTierWeights = $record->free_tier_weights;
|
||||
if (!is_array($freeTierWeights) || $freeTierWeights === []) {
|
||||
$this->markFailed($recordId, '免费未选奖池时需提供 free_tier_weights');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$playLogic = new PlayStartLogic();
|
||||
$resultCounts = []; // grid_number => count
|
||||
$tierCounts = []; // tier => count
|
||||
$resultCounts = [];
|
||||
$tierCounts = [];
|
||||
$buffer = [];
|
||||
$done = 0;
|
||||
|
||||
try {
|
||||
for ($i = 0; $i < $sCount; $i++) {
|
||||
$row = $playLogic->simulateOnePlay($config, 0);
|
||||
for ($i = 0; $i < $paidS; $i++) {
|
||||
$row = $playLogic->simulateOnePlay($paidConfig, 0, 0, $paidTierWeights);
|
||||
$this->aggregate($row, $resultCounts, $tierCounts);
|
||||
$buffer[] = $this->rowForInsert($row);
|
||||
$buffer[] = $this->rowForInsert($row, $recordId);
|
||||
$done++;
|
||||
$this->flushIfNeeded($buffer, $recordId, $done, $total, $resultCounts, $tierCounts);
|
||||
}
|
||||
for ($i = 0; $i < $nCount; $i++) {
|
||||
$row = $playLogic->simulateOnePlay($config, 1);
|
||||
for ($i = 0; $i < $paidN; $i++) {
|
||||
$row = $playLogic->simulateOnePlay($paidConfig, 1, 0, $paidTierWeights);
|
||||
$this->aggregate($row, $resultCounts, $tierCounts);
|
||||
$buffer[] = $this->rowForInsert($row);
|
||||
$buffer[] = $this->rowForInsert($row, $recordId);
|
||||
$done++;
|
||||
$this->flushIfNeeded($buffer, $recordId, $done, $total, $resultCounts, $tierCounts);
|
||||
}
|
||||
for ($i = 0; $i < $freeS; $i++) {
|
||||
$row = $playLogic->simulateOnePlay($freeConfig, 0, 1, $freeTierWeights);
|
||||
$this->aggregate($row, $resultCounts, $tierCounts);
|
||||
$buffer[] = $this->rowForInsert($row, $recordId);
|
||||
$done++;
|
||||
$this->flushIfNeeded($buffer, $recordId, $done, $total, $resultCounts, $tierCounts);
|
||||
}
|
||||
for ($i = 0; $i < $freeN; $i++) {
|
||||
$row = $playLogic->simulateOnePlay($freeConfig, 1, 1, $freeTierWeights);
|
||||
$this->aggregate($row, $resultCounts, $tierCounts);
|
||||
$buffer[] = $this->rowForInsert($row, $recordId);
|
||||
$done++;
|
||||
$this->flushIfNeeded($buffer, $recordId, $done, $total, $resultCounts, $tierCounts);
|
||||
}
|
||||
@@ -68,6 +121,7 @@ class WeightTestRunner
|
||||
$this->insertBuffer($buffer);
|
||||
$this->updateProgress($recordId, $done, $resultCounts, $tierCounts);
|
||||
}
|
||||
// 平台赚取金额:通过关联 DicePlayRecordTest(reward_config_record_id)统计
|
||||
$this->markSuccess($recordId, $resultCounts, $tierCounts);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('WeightTestRunner exception: ' . $e->getMessage(), ['record_id' => $recordId, 'trace' => $e->getTraceAsString()]);
|
||||
@@ -87,9 +141,11 @@ class WeightTestRunner
|
||||
}
|
||||
}
|
||||
|
||||
private function rowForInsert(array $row): array
|
||||
private function rowForInsert(array $row, int $rewardConfigRecordId): array
|
||||
{
|
||||
$out = [];
|
||||
$out = [
|
||||
'reward_config_record_id' => $rewardConfigRecordId,
|
||||
];
|
||||
$keys = [
|
||||
'player_id', 'admin_id', 'lottery_config_id', 'lottery_type', 'is_win', 'win_coin',
|
||||
'super_win_coin', 'reward_win_coin', 'use_coins', 'direction', 'reward_config_id',
|
||||
@@ -134,14 +190,20 @@ class WeightTestRunner
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记测试成功并记录平台总盈利 platform_profit
|
||||
* 通过关联 DicePlayRecordTest(reward_config_record_id)统计:付费(lottery_type=0)次数×100 - win_coin 求和
|
||||
*/
|
||||
private function markSuccess(int $recordId, array $resultCounts, array $tierCounts): void
|
||||
{
|
||||
$platformProfit = DiceRewardConfigRecord::computePlatformProfitFromRelated($recordId);
|
||||
$record = DiceRewardConfigRecord::find($recordId);
|
||||
if ($record) {
|
||||
$record->status = DiceRewardConfigRecord::STATUS_SUCCESS;
|
||||
$record->result_counts = $resultCounts;
|
||||
$record->tier_counts = $tierCounts;
|
||||
$record->remark = null;
|
||||
$record->platform_profit = $platformProfit;
|
||||
$record->save();
|
||||
}
|
||||
}
|
||||
@@ -149,8 +211,9 @@ class WeightTestRunner
|
||||
private function markFailed(int $recordId, string $message): void
|
||||
{
|
||||
DiceRewardConfigRecord::where('id', $recordId)->update([
|
||||
'status' => DiceRewardConfigRecord::STATUS_FAIL,
|
||||
'remark' => mb_substr($message, 0, 500),
|
||||
'status' => DiceRewardConfigRecord::STATUS_FAIL,
|
||||
'remark' => mb_substr($message, 0, 500),
|
||||
'platform_profit' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace app\dice\model\play_record_test;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use app\dice\model\reward_config\DiceRewardConfig;
|
||||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use think\model\relation\BelongsTo;
|
||||
|
||||
@@ -33,6 +34,7 @@ use think\model\relation\BelongsTo;
|
||||
* @property $super_win_coin 中大奖平台币
|
||||
* @property $reward_win_coin 摇色子中奖平台币
|
||||
* @property $admin_id 所属管理员
|
||||
* @property int|null $reward_config_record_id 关联 DiceRewardConfigRecord.id(权重测试记录)
|
||||
*/
|
||||
class DicePlayRecordTest extends BaseModel
|
||||
{
|
||||
@@ -66,6 +68,15 @@ class DicePlayRecordTest extends BaseModel
|
||||
return $this->belongsTo(DiceRewardConfig::class, 'reward_config_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联的权重测试记录
|
||||
* reward_config_record_id -> DiceRewardConfigRecord.id
|
||||
*/
|
||||
public function diceRewardConfigRecord(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(DiceRewardConfigRecord::class, 'reward_config_record_id', 'id');
|
||||
}
|
||||
|
||||
/** 抽奖类型 0=付费 1=赠送 */
|
||||
public function searchLotteryTypeAttr($query, $value)
|
||||
{
|
||||
@@ -119,4 +130,12 @@ class DicePlayRecordTest extends BaseModel
|
||||
$query->whereRaw('1=0');
|
||||
}
|
||||
}
|
||||
|
||||
/** 点数和 roll_number(摇取点数和 5-30) */
|
||||
public function searchRollNumberAttr($query, $value)
|
||||
{
|
||||
if ($value !== '' && $value !== null) {
|
||||
$query->where('roll_number', '=', $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\reward_config_record;
|
||||
|
||||
use app\dice\model\play_record_test\DicePlayRecordTest;
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
use think\model\relation\HasMany;
|
||||
|
||||
/**
|
||||
* 奖励配置权重测试记录模型
|
||||
@@ -17,15 +19,24 @@ use plugin\saiadmin\basic\think\BaseModel;
|
||||
* @property int $test_count 测试次数 100/500/1000/5000/10000
|
||||
* @property array $weight_config_snapshot 测试时权重配比快照:按档位 id,grid_number,tier,weight
|
||||
* @property array $tier_weights_snapshot 测试时 T1-T5 档位权重快照(来自奖池配置)
|
||||
* @property int|null $lottery_config_id 测试时使用的奖池配置 ID
|
||||
* @property int|null $lottery_config_id 测试时使用的奖池配置 ID(兼容旧:付费+免费共用)
|
||||
* @property int|null $paid_lottery_config_id 付费抽奖奖池配置 ID,默认 type=0
|
||||
* @property int|null $free_lottery_config_id 免费抽奖奖池配置 ID,默认 type=1
|
||||
* @property int $total_play_count 总模拟次数(s_count+n_count)
|
||||
* @property int $over_play_count 已完成次数
|
||||
* @property int $status 状态 -1失败 0进行中 1成功
|
||||
* @property string|null $remark 失败时记录原因
|
||||
* @property int $s_count 顺时针模拟次数
|
||||
* @property int $n_count 逆时针模拟次数
|
||||
* @property int $s_count 顺时针模拟次数(兼容旧数据)
|
||||
* @property int $n_count 逆时针模拟次数(兼容旧数据)
|
||||
* @property int $paid_s_count 付费抽奖顺时针次数
|
||||
* @property int $paid_n_count 付费抽奖逆时针次数
|
||||
* @property int $free_s_count 免费抽奖顺时针次数
|
||||
* @property int $free_n_count 免费抽奖逆时针次数
|
||||
* @property array|null $paid_tier_weights 付费自定义档位权重 T1-T5
|
||||
* @property array|null $free_tier_weights 免费自定义档位权重 T1-T5
|
||||
* @property array $result_counts 落点统计 grid_number=>出现次数
|
||||
* @property array|null $tier_counts 档位出现次数 T1=>count
|
||||
* @property float|null $platform_profit 平台赚取金额(付费抽取次数×100-玩家总收益)
|
||||
* @property int|null $admin_id 执行测试的管理员ID
|
||||
* @property string|null $create_time 创建时间
|
||||
*/
|
||||
@@ -33,8 +44,10 @@ class DiceRewardConfigRecord extends BaseModel
|
||||
{
|
||||
/** 状态:失败 */
|
||||
public const STATUS_FAIL = -1;
|
||||
/** 状态:进行中 */
|
||||
/** 状态:待执行(队列中) */
|
||||
public const STATUS_RUNNING = 0;
|
||||
/** 状态:执行中(已被某进程领取,防止定时器重入重复执行) */
|
||||
public const STATUS_EXECUTING = 2;
|
||||
/** 状态:成功 */
|
||||
public const STATUS_SUCCESS = 1;
|
||||
|
||||
@@ -42,7 +55,31 @@ class DiceRewardConfigRecord extends BaseModel
|
||||
|
||||
protected $table = 'dice_reward_config_record';
|
||||
|
||||
protected $json = ['weight_config_snapshot', 'tier_weights_snapshot', 'result_counts', 'tier_counts'];
|
||||
protected $json = ['weight_config_snapshot', 'tier_weights_snapshot', 'result_counts', 'tier_counts', 'paid_tier_weights', 'free_tier_weights'];
|
||||
|
||||
protected $jsonAssoc = true;
|
||||
|
||||
/**
|
||||
* 关联的测试抽奖记录(通过 reward_config_record_id)
|
||||
*/
|
||||
public function playRecordTests(): HasMany
|
||||
{
|
||||
return $this->hasMany(DicePlayRecordTest::class, 'reward_config_record_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据关联的 DicePlayRecordTest 统计平台赚取平台币
|
||||
* platform_profit = 关联的付费(lottery_type=0)抽取次数 × 100 - 关联的 win_coin 求和
|
||||
* @param int $recordId dice_reward_config_record.id
|
||||
* @return float
|
||||
*/
|
||||
public static function computePlatformProfitFromRelated(int $recordId): float
|
||||
{
|
||||
$paidCount = DicePlayRecordTest::where('reward_config_record_id', $recordId)
|
||||
->where('lottery_type', 0)
|
||||
->count();
|
||||
$sumWinCoin = (float) DicePlayRecordTest::where('reward_config_record_id', $recordId)
|
||||
->sum('win_coin');
|
||||
return round($paidCount * 100 - $sumWinCoin, 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class WeightTestProcess
|
||||
|
||||
/**
|
||||
* 执行一条待完成的测试记录(status=0)
|
||||
* 先原子更新为 STATUS_EXECUTING,避免定时器 15 秒重入时同一条记录被重复执行(导致顺/逆时针各跑两倍次数)
|
||||
*/
|
||||
private function runOnePending(): void
|
||||
{
|
||||
@@ -35,6 +36,12 @@ class WeightTestProcess
|
||||
return;
|
||||
}
|
||||
$recordId = (int) $record->id;
|
||||
$affected = DiceRewardConfigRecord::where('id', $recordId)
|
||||
->where('status', DiceRewardConfigRecord::STATUS_RUNNING)
|
||||
->update(['status' => DiceRewardConfigRecord::STATUS_EXECUTING]);
|
||||
if ($affected !== 1) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
(new WeightTestRunner())->run($recordId);
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
Reference in New Issue
Block a user