[色子游戏]奖励配置权重测试记录
This commit is contained in:
@@ -11,23 +11,42 @@ export default {
|
||||
*/
|
||||
list(params: Record<string, any>) {
|
||||
return request.get<Api.Common.ApiPage>({
|
||||
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/index',
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/index',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取 DiceLotteryPoolConfig 列表数据,仅含 id、name,用于 lottery_config_id 下拉
|
||||
* @returns DiceLotteryPoolConfig['id','name'] 列表
|
||||
* @returns DiceLotteryPoolConfig['id','name','t1_weight'..'t5_weight'] 列表
|
||||
*/
|
||||
async getOptions(): Promise<Array<{ id: number; name: string }>> {
|
||||
async getOptions(): Promise<
|
||||
Array<{
|
||||
id: number
|
||||
name: string
|
||||
t1_weight: number
|
||||
t2_weight: number
|
||||
t3_weight: number
|
||||
t4_weight: number
|
||||
t5_weight: number
|
||||
}>
|
||||
> {
|
||||
const res = await request.get<any>({
|
||||
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions'
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions'
|
||||
})
|
||||
const rows = (res?.data ?? []) as Array<{ id: number; name: string }>
|
||||
return Array.isArray(rows)
|
||||
? rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') }))
|
||||
: []
|
||||
// 兼容: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 ?? ''),
|
||||
t1_weight: Number(r.t1_weight ?? 0),
|
||||
t2_weight: Number(r.t2_weight ?? 0),
|
||||
t3_weight: Number(r.t3_weight ?? 0),
|
||||
t4_weight: Number(r.t4_weight ?? 0),
|
||||
t5_weight: Number(r.t5_weight ?? 0)
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -37,7 +56,7 @@ export default {
|
||||
*/
|
||||
read(id: number | string) {
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/read?id=' + id
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/read?id=' + id
|
||||
})
|
||||
},
|
||||
|
||||
@@ -48,7 +67,7 @@ export default {
|
||||
*/
|
||||
save(params: Record<string, any>) {
|
||||
return request.post<any>({
|
||||
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/save',
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/save',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
@@ -60,7 +79,7 @@ export default {
|
||||
*/
|
||||
update(params: Record<string, any>) {
|
||||
return request.put<any>({
|
||||
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/update',
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/update',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
@@ -72,7 +91,7 @@ export default {
|
||||
*/
|
||||
delete(params: Record<string, any>) {
|
||||
return request.del<any>({
|
||||
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/destroy',
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/destroy',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
@@ -92,7 +111,7 @@ export default {
|
||||
t5_weight: number
|
||||
profit_amount: number
|
||||
}>({
|
||||
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool'
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool'
|
||||
})
|
||||
},
|
||||
|
||||
@@ -108,7 +127,7 @@ export default {
|
||||
t5_weight?: number
|
||||
}) {
|
||||
return request.post<any>({
|
||||
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/updateCurrentPool',
|
||||
url: '/core/dice/lottery_pool_config/DiceLotteryPoolConfig/updateCurrentPool',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,5 +80,22 @@ export default {
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/batchUpdateWeights',
|
||||
data: { items }
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 权重配比测试:按当前配置模拟 N 次抽奖,返回各 grid_number 落点次数
|
||||
* @param test_count 100 | 500 | 1000
|
||||
* @param save_record 是否保存到 dice_reward_config_record
|
||||
* @param lottery_config_id 奖池配置ID(DiceLotteryPoolConfig),用于设定 T1-T5 档位概率;不传则使用 type=0 或均等
|
||||
*/
|
||||
runWeightTest(params: {
|
||||
test_count: number
|
||||
save_record?: boolean
|
||||
lottery_config_id?: number | null
|
||||
}) {
|
||||
return request.post<{ data: { counts: Record<string, number>; record_id: number | null } }>({
|
||||
url: '/core/dice/reward_config/DiceRewardConfig/runWeightTest',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import request from '@/utils/http'
|
||||
|
||||
/**
|
||||
* 奖励配置权重测试记录 API接口
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* 获取数据列表
|
||||
* @param params 搜索参数
|
||||
* @returns 数据列表
|
||||
*/
|
||||
list(params: Record<string, any>) {
|
||||
return request.get<Api.Common.ApiPage>({
|
||||
url: '/dice/reward_config_record/DiceRewardConfigRecord/index',
|
||||
params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 读取数据
|
||||
* @param id 数据ID
|
||||
* @returns 数据详情
|
||||
*/
|
||||
read(id: number | string) {
|
||||
return request.get<Api.Common.ApiData>({
|
||||
url: '/dice/reward_config_record/DiceRewardConfigRecord/read?id=' + id
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建数据
|
||||
* @param params 数据参数
|
||||
* @returns 执行结果
|
||||
*/
|
||||
save(params: Record<string, any>) {
|
||||
return request.post<any>({
|
||||
url: '/dice/reward_config_record/DiceRewardConfigRecord/save',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param params 数据参数
|
||||
* @returns 执行结果
|
||||
*/
|
||||
update(params: Record<string, any>) {
|
||||
return request.put<any>({
|
||||
url: '/dice/reward_config_record/DiceRewardConfigRecord/update',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param id 数据ID
|
||||
* @returns 执行结果
|
||||
*/
|
||||
delete(params: Record<string, any>) {
|
||||
return request.del<any>({
|
||||
url: '/dice/reward_config_record/DiceRewardConfigRecord/destroy',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 导入:将测试记录的权重写入 DiceRewardConfig 与 DiceLotteryPoolConfig,并刷新缓存
|
||||
* @param record_id 测试记录 ID
|
||||
* @param lottery_config_id 可选,导入档位权重到的奖池配置 ID,不传则用记录内的 lottery_config_id
|
||||
*/
|
||||
importFromRecord(params: { record_id: number; lottery_config_id?: number | null }) {
|
||||
return request.post<any>({
|
||||
url: '/dice/reward_config_record/DiceRewardConfigRecord/importFromRecord',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,14 @@
|
||||
>
|
||||
T1-T5 与 BIGWIN 权重配比
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'dice:reward_config:index:index'"
|
||||
type="success"
|
||||
@click="weightTestVisible = true"
|
||||
v-ripple
|
||||
>
|
||||
测试中奖
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
@@ -60,6 +68,8 @@
|
||||
/>
|
||||
<!-- T1-T5、BIGWIN 权重配比弹窗 -->
|
||||
<WeightRatioDialog v-model="weightRatioVisible" @success="refreshData" />
|
||||
<!-- 权重配比测试弹窗 -->
|
||||
<WeightTestDialog v-model="weightTestVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -70,8 +80,10 @@
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import WeightRatioDialog from './modules/weight-ratio-dialog.vue'
|
||||
import WeightTestDialog from './modules/weight-test-dialog.vue'
|
||||
|
||||
const weightRatioVisible = ref(false)
|
||||
const weightTestVisible = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref<Record<string, unknown>>({
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
title="权重配比测试"
|
||||
width="720px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<div v-if="!result" class="test-form">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="奖池配置">
|
||||
<el-select
|
||||
v-model="form.lottery_config_id"
|
||||
placeholder="选择 T1-T5 档位概率来源,不选则使用默认"
|
||||
clearable
|
||||
style="width: 320px"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in lotteryConfigOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.name"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="form-tip">
|
||||
{{
|
||||
selectedTierSummary || '选定奖池的 t1_weight~t5_weight 将作为测试时 T1-T5 的抽取概率'
|
||||
}}
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="测试次数">
|
||||
<el-radio-group v-model="form.test_count">
|
||||
<el-radio :value="100">100 次</el-radio>
|
||||
<el-radio :value="500">500 次</el-radio>
|
||||
<el-radio :value="1000">1000 次</el-radio>
|
||||
<el-radio :value="5000">5000 次</el-radio>
|
||||
<el-radio :value="10000">10000 次</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="保存记录">
|
||||
<el-switch v-model="form.save_record" />
|
||||
<span class="form-tip">保存到「测试记录表」便于后续对比</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-else class="test-result">
|
||||
<div class="result-summary">
|
||||
共模拟 <strong>{{ form.test_count }}</strong> 次抽奖,落点分布如下:
|
||||
</div>
|
||||
<div class="chart-wrap">
|
||||
<ArtBarChart
|
||||
x-axis-name="色子点数 (grid_number)"
|
||||
:x-axis-data="chartLabels"
|
||||
:data="chartData"
|
||||
height="320px"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="result.record_id" class="record-tip"
|
||||
>已保存至测试记录,记录 ID:{{ result.record_id }}</div
|
||||
>
|
||||
</div>
|
||||
<template #footer>
|
||||
<template v-if="!result">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleRun">开始测试</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="primary" @click="handleReset">再测一次</el-button>
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '../../../api/reward_config/index'
|
||||
import lotteryConfigApi from '../../../api/lottery_pool_config/index'
|
||||
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const GRID_NUMBERS = [
|
||||
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
|
||||
30
|
||||
]
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const form = ref({
|
||||
lottery_config_id: null as number | null,
|
||||
test_count: 100 as 100 | 500 | 1000 | 5000 | 10000,
|
||||
save_record: true
|
||||
})
|
||||
|
||||
const lotteryConfigOptions = ref<
|
||||
Array<{
|
||||
id: number
|
||||
name: string
|
||||
t1_weight: number
|
||||
t2_weight: number
|
||||
t3_weight: number
|
||||
t4_weight: number
|
||||
t5_weight: number
|
||||
}>
|
||||
>([])
|
||||
const submitting = ref(false)
|
||||
const result = ref<{ counts: Record<string, number>; record_id: number | null } | null>(null)
|
||||
|
||||
const selectedLotteryConfig = computed(
|
||||
() => lotteryConfigOptions.value.find((opt) => opt.id === form.value.lottery_config_id) || null
|
||||
)
|
||||
|
||||
const selectedTierSummary = computed(() => {
|
||||
const opt = selectedLotteryConfig.value
|
||||
if (!opt) return ''
|
||||
const t1 = opt.t1_weight
|
||||
const t2 = opt.t2_weight
|
||||
const t3 = opt.t3_weight
|
||||
const t4 = opt.t4_weight
|
||||
const t5 = opt.t5_weight
|
||||
const total = t1 + t2 + t3 + t4 + t5
|
||||
if (!total) {
|
||||
return `当前奖池 T1-T5 权重:T1=${t1} T2=${t2} T3=${t3} T4=${t4} T5=${t5}(总和为 0,测试时会按均等档位概率)`
|
||||
}
|
||||
const p = (v: number) => ((v / total) * 100).toFixed(1)
|
||||
return `当前奖池 T1-T5 权重:T1=${t1} (${p(t1)}%),T2=${t2} (${p(t2)}%),T3=${t3} (${p(t3)}%),T4=${t4} (${p(t4)}%),T5=${t5} (${p(t5)}%)`
|
||||
})
|
||||
|
||||
const chartLabels = computed(() => GRID_NUMBERS.map((n) => String(n)))
|
||||
|
||||
const chartData = computed(() => {
|
||||
if (!result.value?.counts) return GRID_NUMBERS.map(() => 0)
|
||||
const counts = result.value.counts
|
||||
return GRID_NUMBERS.map((n) => {
|
||||
const v = counts[String(n)] ?? counts[n]
|
||||
return typeof v === 'number' && !Number.isNaN(v) ? v : 0
|
||||
})
|
||||
})
|
||||
|
||||
async function loadLotteryOptions() {
|
||||
try {
|
||||
const list = await lotteryConfigApi.getOptions()
|
||||
lotteryConfigOptions.value = Array.isArray(list) ? list : []
|
||||
} catch {
|
||||
lotteryConfigOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRun() {
|
||||
submitting.value = true
|
||||
result.value = null
|
||||
try {
|
||||
const res = await api.runWeightTest({
|
||||
test_count: form.value.test_count,
|
||||
save_record: form.value.save_record,
|
||||
lottery_config_id: form.value.lottery_config_id ?? undefined
|
||||
})
|
||||
const data = (res as any)?.data ?? res
|
||||
result.value = {
|
||||
counts: data.counts ?? {},
|
||||
record_id: data.record_id ?? null
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message ?? '测试请求失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
result.value = null
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
result.value = null
|
||||
}
|
||||
|
||||
watch(visible, (open) => {
|
||||
if (open) {
|
||||
loadLotteryOptions()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.test-form {
|
||||
padding: 8px 0;
|
||||
}
|
||||
.form-tip {
|
||||
margin-left: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.test-result {
|
||||
padding: 8px 0;
|
||||
}
|
||||
.result-summary {
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.chart-wrap {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.record-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<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"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 权重快照:显示条数 -->
|
||||
<template #snapshot_summary="{ row }">
|
||||
<span>{{ getSnapshotSummary(row) }}</span>
|
||||
</template>
|
||||
<!-- 落点统计:显示总次数 -->
|
||||
<template #result_summary="{ row }">
|
||||
<span>{{ getResultSummary(row) }}</span>
|
||||
</template>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'dice:reward_config_record:index:read'"
|
||||
type="success"
|
||||
toolTip="查看详情"
|
||||
@click="openDetail(row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'dice:reward_config_record:index:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
<!-- 详情抽屉 -->
|
||||
<DetailDrawer v-model="detailVisible" :record="detailRecord" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '../../api/reward_config_record/index'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import DetailDrawer from './modules/detail-drawer.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 详情抽屉
|
||||
const detailVisible = ref(false)
|
||||
const detailRecord = ref<Record<string, any> | null>(null)
|
||||
function openDetail(row: Record<string, any>) {
|
||||
detailRecord.value = { ...row }
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 权重快照摘要:条数(兼容后端返回数组或字符串)
|
||||
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 '—'
|
||||
}
|
||||
}
|
||||
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 '—'
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: { limit: 100 },
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: 'ID', width: 80, align: 'center' },
|
||||
{ prop: 'test_count', label: '测试次数', width: 100, align: 'center' },
|
||||
{
|
||||
prop: 'admin_name',
|
||||
label: '管理员',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'snapshot_summary',
|
||||
label: '权重快照',
|
||||
width: 110,
|
||||
align: 'center',
|
||||
useSlot: true
|
||||
},
|
||||
{
|
||||
prop: 'result_summary',
|
||||
label: '落点统计',
|
||||
width: 150,
|
||||
align: 'center',
|
||||
useSlot: true
|
||||
},
|
||||
{ prop: 'create_time', label: '创建时间', width: 170, align: 'center' },
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
useSlot: true
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
deleteRow,
|
||||
deleteSelectedRows,
|
||||
handleSelectionChange,
|
||||
selectedRows
|
||||
} = useSaiAdmin()
|
||||
</script>
|
||||
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="测试记录详情"
|
||||
:size="drawerSize"
|
||||
direction="rtl"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<template v-if="record">
|
||||
<div class="detail-section">
|
||||
<div class="section-title">基本信息</div>
|
||||
<el-descriptions :column="2" border size="small">
|
||||
<el-descriptions-item label="记录ID">
|
||||
{{ record.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="测试次数">{{ record.test_count }} 次</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ record.create_time || '—' }}
|
||||
</el-descriptions-item>
|
||||
<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>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-title">T1-T5 档位权重配比(测试时使用的奖池配比)</div>
|
||||
<el-table
|
||||
v-if="tierWeightsTableData.length"
|
||||
:data="tierWeightsTableData"
|
||||
border
|
||||
size="small"
|
||||
class="tier-weights-table"
|
||||
max-height="200"
|
||||
>
|
||||
<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)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-title">权重配比快照(测试时使用的 T1-T5/BIGWIN 配置)</div>
|
||||
<el-table
|
||||
:data="snapshotTableData"
|
||||
border
|
||||
size="small"
|
||||
max-height="280"
|
||||
class="snapshot-table"
|
||||
>
|
||||
<el-table-column prop="tier" label="档位" width="80" align="center" />
|
||||
<el-table-column prop="grid_number" label="色子点数" width="100" align="center" />
|
||||
<el-table-column prop="weight" label="权重" width="90" align="center" />
|
||||
<el-table-column prop="id" label="配置ID" width="80" align="center" />
|
||||
</el-table>
|
||||
<div v-if="!snapshotTableData.length" class="empty-tip">暂无快照数据</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<div class="section-title">落点统计(各 grid_number 出现次数)</div>
|
||||
<div class="chart-wrap">
|
||||
<ArtBarChart
|
||||
x-axis-name="色子点数 (grid_number)"
|
||||
:x-axis-data="chartLabels"
|
||||
:data="chartData"
|
||||
height="280px"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="resultTotal === 0" class="empty-tip">暂无落点数据</div>
|
||||
<div v-else class="result-summary">总落点次数:{{ resultTotal }}</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section footer-actions">
|
||||
<el-button type="primary" :loading="importing" @click="openImport">
|
||||
导入到当前配置
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<el-dialog
|
||||
v-model="importVisible"
|
||||
title="导入权重到正式配置"
|
||||
width="460px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<p class="import-desc">
|
||||
将本测试记录的「权重配比快照」写入 DiceRewardConfig,将「T1-T5
|
||||
档位权重」写入所选奖池配置,并刷新缓存。
|
||||
</p>
|
||||
<el-form label-width="120px">
|
||||
<el-form-item label="导入档位到奖池">
|
||||
<el-select
|
||||
v-model="importLotteryConfigId"
|
||||
placeholder="选择要写入 T1-T5 权重的奖池配置"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in lotteryConfigOptions"
|
||||
:key="opt.id"
|
||||
:label="opt.name"
|
||||
:value="opt.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="form-tip">不选则使用本记录保存时的奖池配置 ID</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="importVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="importing" @click="confirmImport">确认导入</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
||||
import recordApi from '../../../api/reward_config_record/index'
|
||||
import lotteryConfigApi from '../../../api/lottery_pool_config/index'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const GRID_NUMBERS = [
|
||||
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
|
||||
30
|
||||
]
|
||||
|
||||
interface RecordRow {
|
||||
id?: number
|
||||
test_count?: number
|
||||
create_time?: string
|
||||
admin_id?: number | null
|
||||
admin_name?: string
|
||||
lottery_config_id?: number | null
|
||||
tier_weights_snapshot?: Record<string, number>
|
||||
weight_config_snapshot?: Array<{
|
||||
id?: number
|
||||
grid_number?: number
|
||||
tier?: string
|
||||
weight?: number
|
||||
}>
|
||||
result_counts?: Record<string, number>
|
||||
}
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
record: RecordRow | null
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'import-done'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
record: null
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const drawerSize = ref('720px')
|
||||
|
||||
const importVisible = ref(false)
|
||||
const importing = ref(false)
|
||||
const importLotteryConfigId = ref<number | null>(null)
|
||||
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
|
||||
|
||||
const tierWeightsTableData = computed(() => {
|
||||
const t = props.record?.tier_weights_snapshot
|
||||
if (!t || typeof t !== 'object') return []
|
||||
const tiers = ['T1', 'T2', 'T3', 'T4', 'T5']
|
||||
const rows = tiers.map((tier) => {
|
||||
const w = t[tier] ?? t[tier.toLowerCase()] ?? 0
|
||||
return { tier, weight: w }
|
||||
})
|
||||
const total = rows.reduce((sum, r) => sum + r.weight, 0)
|
||||
return rows.map((r) => ({
|
||||
tier: r.tier,
|
||||
weight: r.weight,
|
||||
percent: total > 0 ? `${((r.weight / total) * 100).toFixed(1)}%` : '—'
|
||||
}))
|
||||
})
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const snapshotTableData = computed(() => {
|
||||
const snapshot = props.record?.weight_config_snapshot
|
||||
if (!Array.isArray(snapshot)) return []
|
||||
return snapshot.map((item) => ({
|
||||
id: item.id ?? '—',
|
||||
grid_number: item.grid_number ?? '—',
|
||||
tier: item.tier ?? '—',
|
||||
weight: item.weight ?? '—'
|
||||
}))
|
||||
})
|
||||
|
||||
const chartLabels = computed(() => GRID_NUMBERS.map((n) => String(n)))
|
||||
|
||||
const chartData = computed(() => {
|
||||
const counts = props.record?.result_counts
|
||||
if (!counts || typeof counts !== 'object') return GRID_NUMBERS.map(() => 0)
|
||||
return GRID_NUMBERS.map((n) => {
|
||||
const v = counts[String(n)] ?? counts[n]
|
||||
return typeof v === 'number' && !Number.isNaN(v) ? v : 0
|
||||
})
|
||||
})
|
||||
|
||||
const resultTotal = computed(() => {
|
||||
const data = chartData.value
|
||||
return data.reduce((sum, n) => sum + n, 0)
|
||||
})
|
||||
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
importVisible.value = false
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const updateDrawerSize = () => {
|
||||
drawerSize.value = window.innerWidth <= 768 ? '100%' : '720px'
|
||||
}
|
||||
updateDrawerSize()
|
||||
window.addEventListener('resize', updateDrawerSize)
|
||||
}
|
||||
|
||||
async function loadLotteryOptions() {
|
||||
try {
|
||||
const list = await lotteryConfigApi.getOptions()
|
||||
lotteryConfigOptions.value = Array.isArray(list) ? list : []
|
||||
} catch {
|
||||
lotteryConfigOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function openImport() {
|
||||
importLotteryConfigId.value = props.record?.lottery_config_id ?? null
|
||||
importVisible.value = true
|
||||
loadLotteryOptions()
|
||||
}
|
||||
|
||||
async function confirmImport() {
|
||||
const id = props.record?.id
|
||||
if (!id) return
|
||||
importing.value = true
|
||||
try {
|
||||
await recordApi.importFromRecord({
|
||||
record_id: id,
|
||||
lottery_config_id: importLotteryConfigId.value ?? undefined
|
||||
})
|
||||
ElMessage.success('导入成功,已刷新奖励配置与奖池配置')
|
||||
importVisible.value = false
|
||||
emit('import-done')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message ?? '导入失败')
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__body) {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
max-height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.snapshot-table {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.chart-wrap {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.empty-tip {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
.result-summary {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.tier-weights-table {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.footer-actions {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
.import-desc {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增奖励配置权重测试记录' : '编辑奖励配置权重测试记录'"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="测试次数:100/500/1000" prop="test_count">
|
||||
<el-input v-model="formData.test_count" placeholder="请输入测试次数:100/500/1000" />
|
||||
</el-form-item>
|
||||
<el-form-item label="测试时权重配比快照:按档位保存 id,grid_number,tier,weight" prop="weight_config_snapshot">
|
||||
<el-input v-model="formData.weight_config_snapshot" placeholder="请输入测试时权重配比快照:按档位保存 id,grid_number,tier,weight" />
|
||||
</el-form-item>
|
||||
<el-form-item label="落点统计:grid_number=>出现次数" prop="result_counts">
|
||||
<el-input v-model="formData.result_counts" placeholder="请输入落点统计:grid_number=>出现次数" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '../../../api/reward_config_record/index'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
test_count: [{ required: true, message: '测试次数:100/500/1000必需填写', trigger: 'blur' }],
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
test_count: 100,
|
||||
weight_config_snapshot: '',
|
||||
result_counts: '',
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -30,16 +30,26 @@ class DiceLotteryPoolConfigController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 DiceLotteryPoolConfig 列表数据,仅含 id、name,用于 lottery_config_id 下拉(值为 id,显示为 name)
|
||||
* 获取 DiceLotteryPoolConfig 列表数据,用于 lottery_config_id 下拉(值为 id,显示为 name),并附带 T1-T5 档位权重
|
||||
* @param Request $request
|
||||
* @return Response 返回 [ ['id' => int, 'name' => string], ... ]
|
||||
* @return Response 返回 [ ['id' => int, 'name' => string, 't1_weight' => int, ... 't5_weight' => int], ... ]
|
||||
*/
|
||||
#[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
|
||||
public function getOptions(Request $request): Response
|
||||
{
|
||||
$list = DiceLotteryPoolConfig::field('id,name')->order('id', 'asc')->select();
|
||||
$list = DiceLotteryPoolConfig::field('id,name,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'] ?? '')];
|
||||
return [
|
||||
'id' => (int) $item['id'],
|
||||
'name' => (string) ($item['name'] ?? ''),
|
||||
't1_weight' => (int) ($item['t1_weight'] ?? 0),
|
||||
't2_weight' => (int) ($item['t2_weight'] ?? 0),
|
||||
't3_weight' => (int) ($item['t3_weight'] ?? 0),
|
||||
't4_weight' => (int) ($item['t4_weight'] ?? 0),
|
||||
't5_weight' => (int) ($item['t5_weight'] ?? 0),
|
||||
];
|
||||
})->toArray();
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
@@ -155,4 +155,29 @@ class DiceRewardConfigController extends BaseController
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权重配比测试:仅模拟落点统计,不创建游玩记录。按当前配置在内存中模拟 N 次抽奖,返回各 grid_number 落点次数,可选保存到 dice_reward_config_record。
|
||||
* @param Request $request test_count: 100|500|1000, save_record: bool, lottery_config_id: int|null 奖池配置ID,用于设定 T1-T5 概率
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('奖励配置列表', 'dice:reward_config:index:index')]
|
||||
public function runWeightTest(Request $request): Response
|
||||
{
|
||||
$testCount = (int) $request->post('test_count', 100);
|
||||
$saveRecord = (bool) $request->post('save_record', true);
|
||||
$adminId = isset($this->adminInfo['id']) ? (int) $this->adminInfo['id'] : null;
|
||||
$lotteryConfigId = $request->post('lottery_config_id', null);
|
||||
if ($lotteryConfigId !== null && $lotteryConfigId !== '') {
|
||||
$lotteryConfigId = (int) $lotteryConfigId;
|
||||
} else {
|
||||
$lotteryConfigId = null;
|
||||
}
|
||||
try {
|
||||
$result = $this->logic->runWeightTest($testCount, $saveRecord, $adminId, $lotteryConfigId);
|
||||
return $this->success($result);
|
||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: your name
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\controller\reward_config_record;
|
||||
|
||||
use app\dice\logic\reward_config_record\DiceRewardConfigRecordLogic;
|
||||
use app\dice\validate\reward_config_record\DiceRewardConfigRecordValidate;
|
||||
use plugin\saiadmin\basic\BaseController;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
use plugin\saiadmin\service\Permission;
|
||||
use support\Request;
|
||||
use support\Response;
|
||||
|
||||
/**
|
||||
* 奖励配置权重测试记录控制器
|
||||
*/
|
||||
class DiceRewardConfigRecordController extends BaseController
|
||||
{
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->logic = new DiceRewardConfigRecordLogic();
|
||||
$this->validate = new DiceRewardConfigRecordValidate;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('奖励配置权重测试记录列表', 'dice:reward_config_record:index:index')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$where = $request->more([
|
||||
]);
|
||||
$query = $this->logic->search($where);
|
||||
$data = $this->logic->getList($query);
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('奖励配置权重测试记录读取', 'dice:reward_config_record:index:read')]
|
||||
public function read(Request $request): Response
|
||||
{
|
||||
$id = $request->input('id', '');
|
||||
$model = $this->logic->read($id);
|
||||
if ($model) {
|
||||
$data = is_array($model) ? $model : $model->toArray();
|
||||
$data['admin_name'] = $this->getAdminName((int) ($data['admin_id'] ?? 0));
|
||||
return $this->success($data);
|
||||
} else {
|
||||
return $this->fail('未查找到信息');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据管理员 ID 获取姓名(realname 优先,否则 username)
|
||||
*/
|
||||
private function getAdminName(int $adminId): string
|
||||
{
|
||||
if ($adminId <= 0) {
|
||||
return '—';
|
||||
}
|
||||
$user = SystemUser::where('id', $adminId)->field('id,realname,username')->find();
|
||||
if (!$user) {
|
||||
return '';
|
||||
}
|
||||
$user = is_array($user) ? $user : $user->toArray();
|
||||
$name = trim((string) ($user['realname'] ?? ''));
|
||||
if ($name !== '') {
|
||||
return $name;
|
||||
}
|
||||
$name = trim((string) ($user['username'] ?? ''));
|
||||
return $name !== '' ? $name : (string) $adminId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('奖励配置权重测试记录添加', 'dice:reward_config_record:index:save')]
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('save', $data);
|
||||
$result = $this->logic->add($data);
|
||||
if ($result) {
|
||||
return $this->success('添加成功');
|
||||
} else {
|
||||
return $this->fail('添加失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('奖励配置权重测试记录修改', 'dice:reward_config_record:index:update')]
|
||||
public function update(Request $request): Response
|
||||
{
|
||||
$data = $request->post();
|
||||
$this->validate('update', $data);
|
||||
$result = $this->logic->edit($data['id'], $data);
|
||||
if ($result) {
|
||||
return $this->success('修改成功');
|
||||
} else {
|
||||
return $this->fail('修改失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
#[Permission('奖励配置权重测试记录删除', 'dice:reward_config_record:index:destroy')]
|
||||
public function destroy(Request $request): Response
|
||||
{
|
||||
$ids = $request->post('ids', '');
|
||||
if (empty($ids)) {
|
||||
return $this->fail('请选择要删除的数据');
|
||||
}
|
||||
$result = $this->logic->destroy($ids);
|
||||
if ($result) {
|
||||
return $this->success('删除成功');
|
||||
} else {
|
||||
return $this->fail('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入:将测试记录的权重写入 DiceRewardConfig 与 DiceLotteryPoolConfig,并重新实例化缓存
|
||||
* @param Request $request record_id: 测试记录ID, lottery_config_id: 可选,导入档位权重到的奖池配置ID,不传则用记录内的 lottery_config_id
|
||||
* @return Response
|
||||
*/
|
||||
#[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;
|
||||
}
|
||||
try {
|
||||
$this->logic->importFromRecord($recordId, $lotteryConfigId);
|
||||
return $this->success('导入成功,已刷新奖励配置与奖池配置');
|
||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||
return $this->fail($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\reward_config;
|
||||
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use app\dice\model\reward_config\DiceRewardConfig;
|
||||
use app\dice\model\reward_config\DiceRewardConfigRecord;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\utils\Helper;
|
||||
use app\dice\model\reward_config\DiceRewardConfig;
|
||||
use support\Log;
|
||||
|
||||
/**
|
||||
@@ -135,4 +137,160 @@ class DiceRewardConfigLogic extends BaseLogic
|
||||
}
|
||||
DiceRewardConfig::refreshCache();
|
||||
}
|
||||
|
||||
/** 测试时档位权重均为 0 的异常标识 */
|
||||
private const EXCEPTION_WEIGHT_ALL_ZERO = 'REWARD_WEIGHT_ALL_ZERO';
|
||||
|
||||
/**
|
||||
* 按权重抽取一条配置(与 PlayStartLogic 抽奖逻辑一致,仅 weight>0 参与)
|
||||
*/
|
||||
private static function drawRewardByWeight(array $rewards): array
|
||||
{
|
||||
if (empty($rewards)) {
|
||||
throw new \InvalidArgumentException('rewards 不能为空');
|
||||
}
|
||||
$candidateWeights = [];
|
||||
foreach ($rewards as $i => $row) {
|
||||
$w = isset($row['weight']) ? (float) $row['weight'] : 0.0;
|
||||
if ($w > 0) {
|
||||
$candidateWeights[$i] = $w;
|
||||
}
|
||||
}
|
||||
$total = (float) array_sum($candidateWeights);
|
||||
if ($total > 0) {
|
||||
$r = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX) * $total;
|
||||
$acc = 0.0;
|
||||
foreach ($candidateWeights as $i => $w) {
|
||||
$acc += $w;
|
||||
if ($r < $acc) {
|
||||
return $rewards[$i];
|
||||
}
|
||||
}
|
||||
return $rewards[array_key_last($candidateWeights)];
|
||||
}
|
||||
throw new \RuntimeException(self::EXCEPTION_WEIGHT_ALL_ZERO);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按档位权重数组抽取 T1-T5
|
||||
*/
|
||||
private static function drawTierByWeightArray(array $tiers, array $weights): string
|
||||
{
|
||||
$total = array_sum($weights);
|
||||
if ($total <= 0) {
|
||||
return $tiers[random_int(0, count($tiers) - 1)];
|
||||
}
|
||||
$r = random_int(1, (int) $total);
|
||||
$acc = 0;
|
||||
foreach ($weights as $i => $w) {
|
||||
$acc += (int) $w;
|
||||
if ($r <= $acc) {
|
||||
return $tiers[$i];
|
||||
}
|
||||
}
|
||||
return $tiers[count($tiers) - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行权重配比测试:仅按当前配置在内存中模拟 N 次抽奖,统计各 grid_number 落点数量。
|
||||
* 不创建任何游玩记录(DicePlayRecord)、不扣券、不写钱包,仅用于验证权重配比效果。
|
||||
*
|
||||
* @param int $testCount 测试次数 100/500/1000/5000/10000
|
||||
* @param bool $saveRecord 是否保存到 dice_reward_config_record(测试记录表,非游玩记录)
|
||||
* @param int|null $adminId 执行人管理员ID
|
||||
* @param int|null $lotteryConfigId 奖池配置ID(DiceLotteryPoolConfig),用于设定 T1-T5 档位概率;不传则使用 type=0 的配置或均等
|
||||
* @return array{counts: array<int,int>, record_id: int|null} counts 为 grid_number=>出现次数
|
||||
*/
|
||||
public function runWeightTest(int $testCount, bool $saveRecord = true, ?int $adminId = null, ?int $lotteryConfigId = null): array
|
||||
{
|
||||
$allowedCounts = [100, 500, 1000, 5000, 10000];
|
||||
if (!in_array($testCount, $allowedCounts, true)) {
|
||||
throw new ApiException('测试次数仅支持 100、500、1000、5000、10000');
|
||||
}
|
||||
|
||||
$grouped = $this->getListGroupedByTier();
|
||||
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||
$tierWeights = [1, 1, 1, 1, 1];
|
||||
$config = null;
|
||||
if ($lotteryConfigId !== null && $lotteryConfigId > 0) {
|
||||
$config = DiceLotteryPoolConfig::find($lotteryConfigId);
|
||||
}
|
||||
if (!$config) {
|
||||
$config = DiceLotteryPoolConfig::where('type', 0)->find();
|
||||
}
|
||||
if ($config) {
|
||||
$tierWeights = [
|
||||
(int) ($config->t1_weight ?? 0),
|
||||
(int) ($config->t2_weight ?? 0),
|
||||
(int) ($config->t3_weight ?? 0),
|
||||
(int) ($config->t4_weight ?? 0),
|
||||
(int) ($config->t5_weight ?? 0),
|
||||
];
|
||||
if (array_sum($tierWeights) <= 0) {
|
||||
$tierWeights = [1, 1, 1, 1, 1];
|
||||
}
|
||||
}
|
||||
|
||||
$counts = [];
|
||||
$maxRetry = 20;
|
||||
for ($i = 0; $i < $testCount; $i++) {
|
||||
$tier = self::drawTierByWeightArray($tiers, $tierWeights);
|
||||
$rewards = $grouped[$tier] ?? [];
|
||||
if (empty($rewards)) {
|
||||
continue;
|
||||
}
|
||||
$attempt = 0;
|
||||
while ($attempt < $maxRetry) {
|
||||
try {
|
||||
$chosen = self::drawRewardByWeight($rewards);
|
||||
$gridNumber = isset($chosen['grid_number']) ? (int) $chosen['grid_number'] : 0;
|
||||
if ($gridNumber >= 5 && $gridNumber <= 30) {
|
||||
$counts[$gridNumber] = ($counts[$gridNumber] ?? 0) + 1;
|
||||
}
|
||||
break;
|
||||
} catch (\RuntimeException $e) {
|
||||
if ($e->getMessage() === self::EXCEPTION_WEIGHT_ALL_ZERO) {
|
||||
$attempt++;
|
||||
continue;
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$snapshot = [];
|
||||
foreach ($grouped as $tierKey => $rows) {
|
||||
foreach ($rows as $row) {
|
||||
$snapshot[] = [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'grid_number' => (int) ($row['grid_number'] ?? 0),
|
||||
'tier' => (string) ($row['tier'] ?? ''),
|
||||
'weight' => (int) ($row['weight'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$tierWeightsSnapshot = [
|
||||
'T1' => $tierWeights[0] ?? 0,
|
||||
'T2' => $tierWeights[1] ?? 0,
|
||||
'T3' => $tierWeights[2] ?? 0,
|
||||
'T4' => $tierWeights[3] ?? 0,
|
||||
'T5' => $tierWeights[4] ?? 0,
|
||||
];
|
||||
$recordId = null;
|
||||
if ($saveRecord) {
|
||||
$record = new DiceRewardConfigRecord();
|
||||
$record->test_count = $testCount;
|
||||
$record->weight_config_snapshot = $snapshot;
|
||||
$record->tier_weights_snapshot = $tierWeightsSnapshot;
|
||||
$record->lottery_config_id = $config ? (int) $config->id : null;
|
||||
$record->result_counts = $counts;
|
||||
$record->admin_id = $adminId;
|
||||
$record->create_time = date('Y-m-d H:i:s');
|
||||
$record->save();
|
||||
$recordId = (int) $record->id;
|
||||
}
|
||||
|
||||
return ['counts' => $counts, 'record_id' => $recordId];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\logic\reward_config_record;
|
||||
|
||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||
use app\dice\model\reward_config\DiceRewardConfig;
|
||||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||
use plugin\saiadmin\basic\think\BaseLogic;
|
||||
use plugin\saiadmin\exception\ApiException;
|
||||
use plugin\saiadmin\app\model\system\SystemUser;
|
||||
|
||||
/**
|
||||
* 奖励配置权重测试记录逻辑层
|
||||
*/
|
||||
class DiceRewardConfigRecordLogic extends BaseLogic
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->model = new DiceRewardConfigRecord();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页列表,并为每条记录附加 admin_name(管理员姓名:realname 或 username)
|
||||
*/
|
||||
public function getList($query): mixed
|
||||
{
|
||||
$result = parent::getList($query);
|
||||
if (!is_array($result)) {
|
||||
return $result;
|
||||
}
|
||||
$rows = $result['data'] ?? $result['records'] ?? null;
|
||||
if (!is_array($rows) || empty($rows)) {
|
||||
return $result;
|
||||
}
|
||||
$adminIds = array_unique(array_filter(array_column($rows, 'admin_id')));
|
||||
$nameMap = $this->getAdminNameMap($adminIds);
|
||||
$key = isset($result['data']) ? 'data' : 'records';
|
||||
foreach ($result[$key] as &$row) {
|
||||
$aid = isset($row['admin_id']) ? (int) $row['admin_id'] : 0;
|
||||
$row['admin_name'] = $nameMap[$aid] ?? ($aid > 0 ? '' : '—');
|
||||
}
|
||||
unset($row);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据管理员 ID 列表获取 id => 姓名(realname 优先,否则 username)
|
||||
* @param array $adminIds
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function getAdminNameMap(array $adminIds): array
|
||||
{
|
||||
if (empty($adminIds)) {
|
||||
return [];
|
||||
}
|
||||
$list = SystemUser::whereIn('id', $adminIds)->field('id,realname,username')->select()->toArray();
|
||||
$map = [];
|
||||
foreach ($list as $user) {
|
||||
$user = is_array($user) ? $user : (array) $user;
|
||||
$id = (int) ($user['id'] ?? 0);
|
||||
$name = trim((string) ($user['realname'] ?? ''));
|
||||
if ($name === '') {
|
||||
$name = trim((string) ($user['username'] ?? ''));
|
||||
}
|
||||
$map[$id] = $name !== '' ? $name : (string) $id;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将测试记录的权重导入到正式配置:weight_config_snapshot → DiceRewardConfig,tier_weights_snapshot → DiceLotteryPoolConfig,并刷新缓存
|
||||
* @param int $recordId 测试记录 ID
|
||||
* @param int|null $lotteryConfigId 要导入档位权重的奖池配置 ID;不传则使用记录中的 lottery_config_id(若有)
|
||||
* @throws ApiException
|
||||
*/
|
||||
public function importFromRecord(int $recordId, ?int $lotteryConfigId = null): void
|
||||
{
|
||||
$record = $this->model->find($recordId);
|
||||
if (!$record) {
|
||||
throw new ApiException('测试记录不存在');
|
||||
}
|
||||
$record = is_array($record) ? $record : $record->toArray();
|
||||
|
||||
$snapshot = $record['weight_config_snapshot'] ?? null;
|
||||
if (is_string($snapshot)) {
|
||||
$snapshot = json_decode($snapshot, true);
|
||||
}
|
||||
if (is_array($snapshot) && !empty($snapshot)) {
|
||||
foreach ($snapshot as $item) {
|
||||
$id = isset($item['id']) ? (int) $item['id'] : 0;
|
||||
$weight = isset($item['weight']) ? (int) $item['weight'] : 1;
|
||||
if ($id > 0) {
|
||||
DiceRewardConfig::where('id', $id)->update(['weight' => $weight]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
if (!$pool) {
|
||||
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),
|
||||
];
|
||||
DiceLotteryPoolConfig::where('id', $targetLotteryId)->update($update);
|
||||
}
|
||||
|
||||
DiceRewardConfig::refreshCache();
|
||||
DiceRewardConfig::clearRequestInstance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\reward_config;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
|
||||
/**
|
||||
* 权重配比测试记录模型
|
||||
*
|
||||
* dice_reward_config_record 保存测试时的权重快照与落点统计
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $test_count 测试次数 100/500/1000/5000/10000
|
||||
* @property array $weight_config_snapshot 测试时权重配比快照
|
||||
* @property array $tier_weights_snapshot 测试时 T1-T5 档位权重快照(来自奖池配置)
|
||||
* @property int|null $lottery_config_id 测试时使用的奖池配置 ID
|
||||
* @property array $result_counts 落点统计 grid_number=>出现次数
|
||||
* @property int|null $admin_id 执行测试的管理员ID
|
||||
* @property string|null $create_time 创建时间
|
||||
*/
|
||||
class DiceRewardConfigRecord extends BaseModel
|
||||
{
|
||||
protected $pk = 'id';
|
||||
|
||||
protected $table = 'dice_reward_config_record';
|
||||
|
||||
protected $json = ['weight_config_snapshot', 'tier_weights_snapshot', 'result_counts'];
|
||||
|
||||
protected $jsonAssoc = true;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: your name
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\model\reward_config_record;
|
||||
|
||||
use plugin\saiadmin\basic\think\BaseModel;
|
||||
|
||||
/**
|
||||
* 奖励配置权重测试记录模型
|
||||
*
|
||||
* dice_reward_config_record 奖励配置权重测试记录
|
||||
*
|
||||
* @property int $id 主键
|
||||
* @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 array $result_counts 落点统计 grid_number=>出现次数
|
||||
* @property int|null $admin_id 执行测试的管理员ID
|
||||
* @property string|null $create_time 创建时间
|
||||
*/
|
||||
class DiceRewardConfigRecord extends BaseModel
|
||||
{
|
||||
protected $pk = 'id';
|
||||
|
||||
protected $table = 'dice_reward_config_record';
|
||||
|
||||
protected $json = ['weight_config_snapshot', 'tier_weights_snapshot', 'result_counts'];
|
||||
|
||||
protected $jsonAssoc = true;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | saiadmin [ saiadmin快速开发框架 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: your name
|
||||
// +----------------------------------------------------------------------
|
||||
namespace app\dice\validate\reward_config_record;
|
||||
|
||||
use plugin\saiadmin\basic\BaseValidate;
|
||||
|
||||
/**
|
||||
* 奖励配置权重测试记录验证器
|
||||
*/
|
||||
class DiceRewardConfigRecordValidate extends BaseValidate
|
||||
{
|
||||
/**
|
||||
* 定义验证规则
|
||||
*/
|
||||
protected $rule = [
|
||||
'test_count' => 'require',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义错误信息
|
||||
*/
|
||||
protected $message = [
|
||||
'test_count' => '测试次数:100/500/1000必须填写',
|
||||
];
|
||||
|
||||
/**
|
||||
* 定义场景
|
||||
*/
|
||||
protected $scene = [
|
||||
'save' => [
|
||||
'test_count',
|
||||
],
|
||||
'update' => [
|
||||
'test_count',
|
||||
],
|
||||
];
|
||||
|
||||
}
|
||||
@@ -105,10 +105,13 @@ Route::group('/core', function () {
|
||||
fastRoute('dice/reward_config/DiceRewardConfig', \app\dice\controller\reward_config\DiceRewardConfigController::class);
|
||||
Route::get('/dice/reward_config/DiceRewardConfig/weightRatioList', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'weightRatioList']);
|
||||
Route::post('/dice/reward_config/DiceRewardConfig/batchUpdateWeights', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'batchUpdateWeights']);
|
||||
Route::post('/dice/reward_config/DiceRewardConfig/runWeightTest', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'runWeightTest']);
|
||||
fastRoute('dice/lottery_pool_config/DiceLotteryPoolConfig', \app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class);
|
||||
Route::get('/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'getOptions']);
|
||||
Route::get('/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'getCurrentPool']);
|
||||
Route::post('/dice/lottery_pool_config/DiceLotteryPoolConfig/updateCurrentPool', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'updateCurrentPool']);
|
||||
fastRoute('dice/reward_config_record/DiceRewardConfigRecord', \app\dice\controller\reward_config_record\DiceRewardConfigRecordController::class);
|
||||
Route::post('/dice/reward_config_record/DiceRewardConfigRecord/importFromRecord', [\app\dice\controller\reward_config_record\DiceRewardConfigRecordController::class, 'importFromRecord']);
|
||||
|
||||
// 数据表维护
|
||||
Route::get("/database/index", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'index']);
|
||||
|
||||
Reference in New Issue
Block a user