优化中奖权重计算方式
This commit is contained in:
@@ -21,8 +21,8 @@ import { HttpError, handleError, showError, showSuccess } from './error'
|
|||||||
import { $t } from '@/locales'
|
import { $t } from '@/locales'
|
||||||
import { BaseResponse } from '@/types'
|
import { BaseResponse } from '@/types'
|
||||||
|
|
||||||
/** 请求配置常量 */
|
/** 请求配置常量(超时时间 30s) */
|
||||||
const REQUEST_TIMEOUT = 15000
|
const REQUEST_TIMEOUT = 30000
|
||||||
const LOGOUT_DELAY = 500
|
const LOGOUT_DELAY = 500
|
||||||
const MAX_RETRIES = 0
|
const MAX_RETRIES = 0
|
||||||
const RETRY_DELAY = 1000
|
const RETRY_DELAY = 1000
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ export const tableConfig = {
|
|||||||
// 总条数
|
// 总条数
|
||||||
totalFields: ['total', 'count'],
|
totalFields: ['total', 'count'],
|
||||||
// 当前页码
|
// 当前页码
|
||||||
currentFields: ['current', 'page', 'pageNum'],
|
currentFields: ['current', 'page', 'pageNum', 'current_page'],
|
||||||
// 每页大小
|
// 每页大小
|
||||||
sizeFields: ['size', 'pageSize', 'limit'],
|
sizeFields: ['size', 'pageSize', 'limit', 'per_page'],
|
||||||
|
|
||||||
// 请求参数映射配置,前端发送请求时使用的分页参数名
|
// 请求参数映射配置,前端发送请求时使用的分页参数名
|
||||||
// useTable 组合式函数传递分页参数的时候 用 current 跟 size
|
// useTable 组合式函数传递分页参数的时候 用 current 跟 size
|
||||||
|
|||||||
61
saiadmin-artd/src/views/plugin/dice/api/reward/index.ts
Normal file
61
saiadmin-artd/src/views/plugin/dice/api/reward/index.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import request from '@/utils/http'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 奖励对照(dice_reward)API
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* 分页列表,按 direction 区分顺时针(0)/逆时针(1)
|
||||||
|
* @param params direction(必), tier(选), page, limit, orderField, orderType
|
||||||
|
*/
|
||||||
|
list(params: Record<string, any>) {
|
||||||
|
return request.get<Api.Common.ApiPage>({
|
||||||
|
url: '/core/dice/reward/DiceReward/index',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权重编辑弹窗:按档位分组获取当前方向的配置+权重(单方向)
|
||||||
|
* @param direction 0=顺时针 1=逆时针
|
||||||
|
*/
|
||||||
|
weightRatioList(direction: 0 | 1) {
|
||||||
|
return request.get<Api.Common.ApiData>({
|
||||||
|
url: '/core/dice/reward/DiceReward/weightRatioList',
|
||||||
|
params: { direction }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权重编辑弹窗:按档位分组获取配置+顺时针/逆时针权重(dice_reward 双方向)
|
||||||
|
*/
|
||||||
|
weightRatioListWithDirection() {
|
||||||
|
return request.get<Api.Common.ApiData>({
|
||||||
|
url: '/core/dice/reward/DiceReward/weightRatioListWithDirection'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权重编辑弹窗:按 DiceReward 主键 id 批量更新 weight
|
||||||
|
* @param items [{ id: DiceReward.id, weight: 1-10000 }, ...]
|
||||||
|
*/
|
||||||
|
batchUpdateWeights(items: Array<{ id: number; weight: number }>) {
|
||||||
|
return request.post<any>({
|
||||||
|
url: '/core/dice/reward/DiceReward/batchUpdateWeights',
|
||||||
|
data: { items }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权重编辑弹窗:批量更新当前方向的权重(单方向)
|
||||||
|
*/
|
||||||
|
batchUpdateWeightsByDirection(
|
||||||
|
direction: 0 | 1,
|
||||||
|
items: Array<{ id: number; weight: number }>
|
||||||
|
) {
|
||||||
|
return request.post<any>({
|
||||||
|
url: '/core/dice/reward/DiceReward/batchUpdateWeightsByDirection',
|
||||||
|
data: { direction, items }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,8 +73,9 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* T1-T5、BIGWIN 权重配比:批量更新权重(同一档位权重之和必须等于 100%)
|
* T1-T5、BIGWIN 权重配比:批量更新顺时针/逆时针权重(写入 dice_reward)
|
||||||
*/
|
*/
|
||||||
|
/** 按 DiceReward 主键 id 批量更新 weight;items: [{ id, weight }, ...] */
|
||||||
batchUpdateWeights(items: Array<{ id: number; weight: number }>) {
|
batchUpdateWeights(items: Array<{ id: number; weight: number }>) {
|
||||||
return request.post<any>({
|
return request.post<any>({
|
||||||
url: '/core/dice/reward_config/DiceRewardConfig/batchUpdateWeights',
|
url: '/core/dice/reward_config/DiceRewardConfig/batchUpdateWeights',
|
||||||
@@ -82,6 +83,20 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建奖励对照:按当前奖励配置为顺时针(0)、逆时针(1)生成所有色子可能对应的 dice_reward 记录,权重默认 1,可在奖励对照页权重编辑中调整
|
||||||
|
*/
|
||||||
|
createRewardReference() {
|
||||||
|
return request.post<{
|
||||||
|
created_clockwise: number
|
||||||
|
created_counterclockwise: number
|
||||||
|
updated_clockwise: number
|
||||||
|
updated_counterclockwise: number
|
||||||
|
}>({
|
||||||
|
url: '/core/dice/reward_config/DiceRewardConfig/createRewardReference'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权重配比测试:按当前配置模拟 N 次抽奖,返回各 grid_number 落点次数
|
* 权重配比测试:按当前配置模拟 N 次抽奖,返回各 grid_number 落点次数
|
||||||
* @param test_count 100 | 500 | 1000
|
* @param test_count 100 | 500 | 1000
|
||||||
|
|||||||
127
saiadmin-artd/src/views/plugin/dice/reward/index/index.vue
Normal file
127
saiadmin-artd/src/views/plugin/dice/reward/index/index.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="art-full-height">
|
||||||
|
<!-- 方向切换 + 搜索 -->
|
||||||
|
<div class="direction-bar">
|
||||||
|
<el-radio-group v-model="currentDirection" size="default" @change="onDirectionChange">
|
||||||
|
<el-radio-button :value="0">顺时针</el-radio-button>
|
||||||
|
<el-radio-button :value="1">逆时针</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<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:index:update'"
|
||||||
|
type="primary"
|
||||||
|
@click="weightRatioVisible = true"
|
||||||
|
v-ripple
|
||||||
|
>
|
||||||
|
权重配比
|
||||||
|
</ElButton>
|
||||||
|
</ElSpace>
|
||||||
|
</template>
|
||||||
|
</ArtTableHeader>
|
||||||
|
|
||||||
|
<ArtTable
|
||||||
|
ref="tableRef"
|
||||||
|
rowKey="id"
|
||||||
|
:loading="loading"
|
||||||
|
:data="data"
|
||||||
|
:columns="columns"
|
||||||
|
:pagination="pagination"
|
||||||
|
@sort-change="handleSortChange"
|
||||||
|
@pagination:size-change="handleSizeChange"
|
||||||
|
@pagination:current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</ElCard>
|
||||||
|
|
||||||
|
<WeightRatioDialog v-model="weightRatioVisible" @success="refreshData" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useTable } from '@/hooks/core/useTable'
|
||||||
|
import api from '../../api/reward/index'
|
||||||
|
import TableSearch from './modules/table-search.vue'
|
||||||
|
import WeightRatioDialog from './modules/weight-ratio-dialog.vue'
|
||||||
|
|
||||||
|
const currentDirection = ref<0 | 1>(0)
|
||||||
|
const weightRatioVisible = ref(false)
|
||||||
|
|
||||||
|
const searchForm = ref<Record<string, unknown>>({
|
||||||
|
direction: 0,
|
||||||
|
tier: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const listApi = (params: Record<string, any>) => {
|
||||||
|
return api.list({ ...params, direction: currentDirection.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = (params: Record<string, any>) => {
|
||||||
|
Object.assign(searchParams, { ...params, direction: currentDirection.value })
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDirectionChange = () => {
|
||||||
|
searchForm.value.direction = currentDirection.value
|
||||||
|
Object.assign(searchParams, { direction: currentDirection.value, tier: searchForm.value.tier })
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
columns,
|
||||||
|
columnChecks,
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
getData,
|
||||||
|
searchParams,
|
||||||
|
pagination,
|
||||||
|
resetSearchParams,
|
||||||
|
handleSortChange,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
refreshData
|
||||||
|
} = useTable({
|
||||||
|
core: {
|
||||||
|
apiFn: listApi,
|
||||||
|
apiParams: { direction: 0, limit: 100 },
|
||||||
|
columnsFactory: () => [
|
||||||
|
{ prop: 'start_index', label: '起始索引', width: 100, align: 'center' },
|
||||||
|
{ prop: 'end_index', label: '结束索引(end_index)', width: 110, align: 'center' },
|
||||||
|
{ prop: 'tier', label: '档位', width: 90, align: 'center', sortable: true },
|
||||||
|
{
|
||||||
|
prop: 'grid_number',
|
||||||
|
label: '色子点数(摇取5-30)',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
sortable: true,
|
||||||
|
showOverflowTooltip: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prop: 'ui_text',
|
||||||
|
label: '显示文本',
|
||||||
|
minWidth: 100,
|
||||||
|
align: 'center',
|
||||||
|
showOverflowTooltip: true
|
||||||
|
},
|
||||||
|
{ prop: 'real_ev', label: '实际中奖金额', width: 110, align: 'center' },
|
||||||
|
{ prop: 'remark', label: '备注', minWidth: 80, align: 'center', showOverflowTooltip: true },
|
||||||
|
{ prop: 'weight', label: '权重(1-10000)', width: 110, align: 'center' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
searchParams.direction = currentDirection.value
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.direction-bar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<sa-search-bar
|
||||||
|
ref="searchBarRef"
|
||||||
|
v-model="formData"
|
||||||
|
label-width="100px"
|
||||||
|
:showExpand="false"
|
||||||
|
@reset="handleReset"
|
||||||
|
@search="handleSearch"
|
||||||
|
>
|
||||||
|
<el-col v-bind="setSpan(8)">
|
||||||
|
<el-form-item label="档位" prop="tier">
|
||||||
|
<el-select v-model="formData.tier" placeholder="全部" clearable style="width: 100%">
|
||||||
|
<el-option label="T1" value="T1" />
|
||||||
|
<el-option label="T2" value="T2" />
|
||||||
|
<el-option label="T3" value="T3" />
|
||||||
|
<el-option label="T4" value="T4" />
|
||||||
|
<el-option label="T5" value="T5" />
|
||||||
|
<el-option label="BIGWIN" value="BIGWIN" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</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 searchBarRef = ref()
|
||||||
|
const formData = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (val) => emit('update:modelValue', val)
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
searchBarRef.value?.ref?.resetFields?.()
|
||||||
|
emit('reset')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
emit('search', { ...formData.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const setSpan = (span: number) => ({
|
||||||
|
span,
|
||||||
|
xs: 24,
|
||||||
|
sm: 12,
|
||||||
|
md: 8,
|
||||||
|
lg: span,
|
||||||
|
xl: span
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,477 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title="奖励对照表(dice_reward)权重配比"
|
||||||
|
width="900px"
|
||||||
|
align-center
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="global-tip">
|
||||||
|
编辑的是<strong>奖励对照表(dice_reward / DiceReward 模型)</strong>的权重,按<strong>结束索引(end_index)</strong>区分
|
||||||
|
<strong>顺时针</strong>与<strong>逆时针</strong>两套权重;抽奖时按当前方向取对应权重。
|
||||||
|
</div>
|
||||||
|
<div v-loading="loading" class="dialog-body">
|
||||||
|
<el-tabs v-model="activeTier" type="card">
|
||||||
|
<el-tab-pane v-for="t in tierKeys" :key="t" :label="t" :name="t">
|
||||||
|
<div v-if="getTierItems(t).length === 0" class="empty-tip">该档位暂无配置数据</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
|
||||||
|
<div class="chart-row">
|
||||||
|
<ArtBarChart
|
||||||
|
x-axis-name="结束索引"
|
||||||
|
:x-axis-data="getTierChartLabels(t)"
|
||||||
|
:data="getTierChartData(t, 'clockwise')"
|
||||||
|
height="180px"
|
||||||
|
/>
|
||||||
|
<ArtBarChart
|
||||||
|
x-axis-name="结束索引"
|
||||||
|
:x-axis-data="getTierChartLabels(t)"
|
||||||
|
:data="getTierChartData(t, 'counterclockwise')"
|
||||||
|
height="180px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
||||||
|
当前档位权重合计(顺时针):<strong>{{ getTierSum(t, 'clockwise') }}</strong>
|
||||||
|
;逆时针:<strong>{{ getTierSum(t, 'counterclockwise') }}</strong>
|
||||||
|
(各条 1-10000,和不限制)
|
||||||
|
</div>
|
||||||
|
<div class="weight-sum weight-sum-t4t5" v-else>T4、T5 仅单一结果,无需配置权重。</div>
|
||||||
|
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
||||||
|
<el-table-column label="结束索引(id)" prop="id" width="90" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="色子点数" prop="grid_number" width="80" align="center" />
|
||||||
|
<el-table-column label="实际中奖金额" prop="real_ev" width="90" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="显示文本" prop="ui_text" min-width="70" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="备注" prop="remark" min-width="70" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="顺时针权重(direction=0)" min-width="160" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="weight-cell-vertical">
|
||||||
|
<div class="weight-slider-wrap">
|
||||||
|
<el-slider
|
||||||
|
:model-value="getItemWeight(row, 'clockwise')"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
size="small"
|
||||||
|
:disabled="isWeightDisabled(row, t)"
|
||||||
|
class="weight-slider"
|
||||||
|
@update:model-value="
|
||||||
|
(v: number | number[]) =>
|
||||||
|
setItemWeightByRow(t, row, 'clockwise', Array.isArray(v) ? (v[0] ?? 1) : (v ?? 1))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="weight-input-wrap">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') <= 1"
|
||||||
|
@click="setItemWeightByRow(t, row, 'clockwise', Math.max(1, getItemWeight(row, 'clockwise') - 1))"
|
||||||
|
>-</el-button>
|
||||||
|
<el-input-number
|
||||||
|
:model-value="getItemWeight(row, 'clockwise')"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
:disabled="isWeightDisabled(row, t)"
|
||||||
|
controls-position="right"
|
||||||
|
size="small"
|
||||||
|
class="weight-input"
|
||||||
|
@update:model-value="
|
||||||
|
(v: number | string | undefined) =>
|
||||||
|
setItemWeightByRow(
|
||||||
|
t,
|
||||||
|
row,
|
||||||
|
'clockwise',
|
||||||
|
typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') >= 10000"
|
||||||
|
@click="setItemWeightByRow(t, row, 'clockwise', Math.min(10000, getItemWeight(row, 'clockwise') + 1))"
|
||||||
|
>+</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="逆时针权重(direction=1)" min-width="160" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="weight-cell-vertical">
|
||||||
|
<div class="weight-slider-wrap">
|
||||||
|
<el-slider
|
||||||
|
:model-value="getItemWeight(row, 'counterclockwise')"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
size="small"
|
||||||
|
:disabled="isWeightDisabled(row, t)"
|
||||||
|
class="weight-slider"
|
||||||
|
@update:model-value="
|
||||||
|
(v: number | number[]) =>
|
||||||
|
setItemWeightByRow(t, row, 'counterclockwise', Array.isArray(v) ? (v[0] ?? 1) : (v ?? 1))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="weight-input-wrap">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'counterclockwise') <= 1"
|
||||||
|
@click="setItemWeightByRow(t, row, 'counterclockwise', Math.max(1, getItemWeight(row, 'counterclockwise') - 1))"
|
||||||
|
>-</el-button>
|
||||||
|
<el-input-number
|
||||||
|
:model-value="getItemWeight(row, 'counterclockwise')"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
:disabled="isWeightDisabled(row, t)"
|
||||||
|
controls-position="right"
|
||||||
|
size="small"
|
||||||
|
class="weight-input"
|
||||||
|
@update:model-value="
|
||||||
|
(v: number | string | undefined) =>
|
||||||
|
setItemWeightByRow(
|
||||||
|
t,
|
||||||
|
row,
|
||||||
|
'counterclockwise',
|
||||||
|
typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'counterclockwise') >= 10000"
|
||||||
|
@click="setItemWeightByRow(t, row, 'counterclockwise', Math.min(10000, getItemWeight(row, 'counterclockwise') + 1))"
|
||||||
|
>+</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmit">提交</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import api from '../../../api/reward/index'
|
||||||
|
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
|
||||||
|
type DirectionKey = 'clockwise' | 'counterclockwise'
|
||||||
|
|
||||||
|
interface WeightRow {
|
||||||
|
reward_id_clockwise?: number
|
||||||
|
reward_id_counterclockwise?: number
|
||||||
|
id?: number
|
||||||
|
grid_number?: number
|
||||||
|
real_ev?: number
|
||||||
|
ui_text?: string
|
||||||
|
remark?: string
|
||||||
|
tier?: string
|
||||||
|
weight_clockwise: number
|
||||||
|
weight_counterclockwise: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'success'): 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 activeTier = ref('T1')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const grouped = ref<Record<string, WeightRow[]>>({
|
||||||
|
T1: [],
|
||||||
|
T2: [],
|
||||||
|
T3: [],
|
||||||
|
T4: [],
|
||||||
|
T5: []
|
||||||
|
})
|
||||||
|
|
||||||
|
function getTierItems(tier: string): WeightRow[] {
|
||||||
|
return grouped.value[tier] ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTierChartLabels(tier: string): string[] {
|
||||||
|
return getTierItems(tier).map((r) => String(r.grid_number ?? ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTierChartData(tier: string, dir: DirectionKey): number[] {
|
||||||
|
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||||
|
return getTierItems(tier).map((r) => toWeightPrecision(r[key] ?? 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTierSum(tier: string, dir: DirectionKey): number {
|
||||||
|
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||||
|
return getTierItems(tier).reduce((s, r) => s + toWeightPrecision(r[key] ?? 1), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemWeight(row: WeightRow, dir: DirectionKey): number {
|
||||||
|
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||||
|
const w = row[key]
|
||||||
|
const num = typeof w === 'number' && !Number.isNaN(w) ? w : Number(w)
|
||||||
|
if (Number.isNaN(num)) return 1
|
||||||
|
return toWeightPrecision(num)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWeightPrecision(value: number): number {
|
||||||
|
return Math.max(1, Math.min(10000, Math.floor(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setItemWeightByRow(tier: string, row: WeightRow, dir: DirectionKey, value: number) {
|
||||||
|
const v = toWeightPrecision(typeof value === 'number' && !Number.isNaN(value) ? value : 1)
|
||||||
|
const list = grouped.value[tier]
|
||||||
|
if (!list) return
|
||||||
|
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||||
|
const rid =
|
||||||
|
dir === 'clockwise' ? row.reward_id_clockwise : row.reward_id_counterclockwise
|
||||||
|
const idx = list.findIndex(
|
||||||
|
(r) =>
|
||||||
|
r === row ||
|
||||||
|
(rid != null &&
|
||||||
|
(dir === 'clockwise' ? r.reward_id_clockwise : r.reward_id_counterclockwise) === rid)
|
||||||
|
)
|
||||||
|
if (idx >= 0) list[idx][key] = v
|
||||||
|
else row[key] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWeightDisabled(row: WeightRow, tier: string): boolean {
|
||||||
|
if (tier === 'T4' || tier === 'T5') return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWeightValue(v: unknown): number {
|
||||||
|
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
||||||
|
if (Number.isNaN(num)) return 1
|
||||||
|
return Math.max(1, Math.min(10000, Math.floor(num)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 兼容接口返回 tier -> { 0: [], 1: [] },按 grid_number 合并为每档位一行,含 reward_id 与双方向权重 */
|
||||||
|
function parsePayload(res: any): Record<string, WeightRow[]> {
|
||||||
|
const empty: Record<string, WeightRow[]> = {
|
||||||
|
T1: [],
|
||||||
|
T2: [],
|
||||||
|
T3: [],
|
||||||
|
T4: [],
|
||||||
|
T5: []
|
||||||
|
}
|
||||||
|
if (!res || typeof res !== 'object') return empty
|
||||||
|
const raw = res?.data ?? res
|
||||||
|
if (!raw || typeof raw !== 'object') return empty
|
||||||
|
const out = { ...empty }
|
||||||
|
for (const t of TIER_KEYS) {
|
||||||
|
const tierData = raw[t]
|
||||||
|
if (!tierData || typeof tierData !== 'object') continue
|
||||||
|
const list0 = Array.isArray(tierData[0]) ? tierData[0] : []
|
||||||
|
const list1 = Array.isArray(tierData[1]) ? tierData[1] : []
|
||||||
|
const byGrid = new Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
reward_id_clockwise?: number
|
||||||
|
reward_id_counterclockwise?: number
|
||||||
|
id?: number
|
||||||
|
grid_number?: number
|
||||||
|
real_ev?: number
|
||||||
|
ui_text?: string
|
||||||
|
remark?: string
|
||||||
|
weight_clockwise: number
|
||||||
|
weight_counterclockwise: number
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
for (const r of list0) {
|
||||||
|
const gn = r.grid_number != null ? Number(r.grid_number) : NaN
|
||||||
|
if (!Number.isNaN(gn)) {
|
||||||
|
byGrid.set(gn, {
|
||||||
|
reward_id_clockwise: r.reward_id != null ? Number(r.reward_id) : undefined,
|
||||||
|
id: r.id != null ? Number(r.id) : undefined,
|
||||||
|
grid_number: gn,
|
||||||
|
real_ev: r.real_ev,
|
||||||
|
ui_text: r.ui_text,
|
||||||
|
remark: r.remark,
|
||||||
|
weight_clockwise: normalizeWeightValue(r.weight),
|
||||||
|
weight_counterclockwise: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const r of list1) {
|
||||||
|
const gn = r.grid_number != null ? Number(r.grid_number) : NaN
|
||||||
|
if (!Number.isNaN(gn)) {
|
||||||
|
const cur = byGrid.get(gn)
|
||||||
|
if (cur) {
|
||||||
|
cur.reward_id_counterclockwise = r.reward_id != null ? Number(r.reward_id) : undefined
|
||||||
|
cur.weight_counterclockwise = normalizeWeightValue(r.weight)
|
||||||
|
} else {
|
||||||
|
byGrid.set(gn, {
|
||||||
|
reward_id_counterclockwise: r.reward_id != null ? Number(r.reward_id) : undefined,
|
||||||
|
id: r.id != null ? Number(r.id) : undefined,
|
||||||
|
grid_number: gn,
|
||||||
|
real_ev: r.real_ev,
|
||||||
|
ui_text: r.ui_text,
|
||||||
|
remark: r.remark,
|
||||||
|
weight_clockwise: 1,
|
||||||
|
weight_counterclockwise: normalizeWeightValue(r.weight)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[t] = Array.from(byGrid.values())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
api
|
||||||
|
.weightRatioListWithDirection()
|
||||||
|
.then((res: any) => {
|
||||||
|
grouped.value = parsePayload(res)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
ElMessage.error('获取权重数据失败')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按 DiceReward 主键 id 收集:每条记录一条 { id, weight } */
|
||||||
|
function collectItems(): Array<{ id: number; weight: number }> {
|
||||||
|
const items: Array<{ id: number; weight: number }> = []
|
||||||
|
for (const t of TIER_KEYS) {
|
||||||
|
for (const row of getTierItems(t)) {
|
||||||
|
const w0 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'clockwise')
|
||||||
|
const w1 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'counterclockwise')
|
||||||
|
const rid0 = row.reward_id_clockwise != null ? row.reward_id_clockwise : 0
|
||||||
|
const rid1 = row.reward_id_counterclockwise != null ? row.reward_id_counterclockwise : 0
|
||||||
|
if (rid0 > 0) items.push({ id: rid0, weight: w0 })
|
||||||
|
if (rid1 > 0) items.push({ id: rid1, weight: w1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
const items = collectItems()
|
||||||
|
if (items.length === 0) {
|
||||||
|
ElMessage.info('没有可提交的配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
api
|
||||||
|
.batchUpdateWeights(items)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
emit('success')
|
||||||
|
handleClose()
|
||||||
|
})
|
||||||
|
.catch((e: { message?: string }) => {
|
||||||
|
ElMessage.error(e?.message ?? '保存失败')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitting.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (open) {
|
||||||
|
loadData()
|
||||||
|
activeTier.value = 'T1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.chart-wrap {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.chart-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.weight-sum {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.weight-sum-t4t5 {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
.global-tip {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.weight-table {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.weight-cell-vertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.weight-slider-wrap {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 80px;
|
||||||
|
padding: 0 8px;
|
||||||
|
:deep(.weight-slider) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.weight-input-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.empty-tip {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
.dialog-body {
|
||||||
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
v-model="visible"
|
||||||
|
title="权重配比"
|
||||||
|
width="900px"
|
||||||
|
align-center
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<div class="global-tip">
|
||||||
|
配置<strong>奖励对照表(dice_reward)</strong>的权重,一级按<strong>方向</strong>(顺时针/逆时针),二级按<strong>档位</strong>(T1-T5);各条权重 1-10000,档位内按权重比抽取。
|
||||||
|
</div>
|
||||||
|
<div v-loading="loading" class="dialog-body">
|
||||||
|
<!-- 一级:方向;二级档位放在各方向 pane 内,切换方向时二级能正常显示 -->
|
||||||
|
<el-tabs v-model="activeDirection" type="card" class="direction-tabs">
|
||||||
|
<el-tab-pane label="顺时针" name="0">
|
||||||
|
<el-tabs v-model="activeTier" type="card" class="tier-tabs">
|
||||||
|
<el-tab-pane v-for="t in tierKeys" :key="'cw-' + t" :label="t" :name="t">
|
||||||
|
<div v-if="getTierItems(t).length === 0" class="empty-tip">该档位暂无配置数据</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
|
||||||
|
<ArtBarChart
|
||||||
|
x-axis-name="点数"
|
||||||
|
:x-axis-data="getTierChartLabels(t)"
|
||||||
|
:data="getTierChartDataForCurrentDirection(t)"
|
||||||
|
height="180px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
||||||
|
当前档位权重合计:<strong>{{ getTierSumForCurrentDirection(t) }}</strong>
|
||||||
|
(各条 1-10000,档位内按权重比抽取,和不限制)
|
||||||
|
</div>
|
||||||
|
<div class="weight-sum weight-sum-t4t5" v-else>T4、T5 仅单一结果,无需配置权重。</div>
|
||||||
|
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
||||||
|
<el-table-column label="点数(grid_number)" prop="grid_number" width="110" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="结束索引(id)" prop="id" width="90" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="实际中奖金额" prop="real_ev" width="90" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="显示文本" prop="ui_text" min-width="70" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="备注" prop="remark" min-width="70" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column :label="currentDirectionLabel + ' 权重(1-10000)'" min-width="200" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="weight-cell-vertical">
|
||||||
|
<div class="weight-slider-wrap">
|
||||||
|
<el-slider
|
||||||
|
:model-value="getItemWeightForCurrentDirection(row)"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
size="small"
|
||||||
|
:disabled="isWeightDisabled(row, t)"
|
||||||
|
class="weight-slider"
|
||||||
|
@update:model-value="(v: number | number[]) => setItemWeightForCurrentDirection(t, row, Array.isArray(v) ? (v[0] ?? 1) : (v ?? 1))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="weight-input-wrap">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) <= 1"
|
||||||
|
@click="setItemWeightForCurrentDirection(t, row, Math.max(1, getItemWeightForCurrentDirection(row) - 1))"
|
||||||
|
>-</el-button>
|
||||||
|
<el-input-number
|
||||||
|
:model-value="getItemWeightForCurrentDirection(row)"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
:disabled="isWeightDisabled(row, t)"
|
||||||
|
controls-position="right"
|
||||||
|
size="small"
|
||||||
|
class="weight-input"
|
||||||
|
@update:model-value="(v: number | string | undefined) => setItemWeightForCurrentDirection(t, row, typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) >= 10000"
|
||||||
|
@click="setItemWeightForCurrentDirection(t, row, Math.min(10000, getItemWeightForCurrentDirection(row) + 1))"
|
||||||
|
>+</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="逆时针 (direction=1)" name="1">
|
||||||
|
<el-tabs v-model="activeTier" type="card" class="tier-tabs">
|
||||||
|
<el-tab-pane v-for="t in tierKeys" :key="'ccw-' + t" :label="t" :name="t">
|
||||||
|
<div v-if="getTierItems(t).length === 0" class="empty-tip">该档位暂无配置数据</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
|
||||||
|
<ArtBarChart
|
||||||
|
x-axis-name="点数"
|
||||||
|
:x-axis-data="getTierChartLabels(t)"
|
||||||
|
:data="getTierChartDataForCurrentDirection(t)"
|
||||||
|
height="180px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
||||||
|
当前档位权重合计:<strong>{{ getTierSumForCurrentDirection(t) }}</strong>
|
||||||
|
(各条 1-10000,档位内按权重比抽取,和不限制)
|
||||||
|
</div>
|
||||||
|
<div class="weight-sum weight-sum-t4t5" v-else>T4、T5 仅单一结果,无需配置权重。</div>
|
||||||
|
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
||||||
|
<el-table-column label="点数(grid_number)" prop="grid_number" width="110" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="结束索引(id)" prop="id" width="90" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="实际中奖金额" prop="real_ev" width="90" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="显示文本" prop="ui_text" min-width="70" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column label="备注" prop="remark" min-width="70" align="center" show-overflow-tooltip />
|
||||||
|
<el-table-column :label="currentDirectionLabel + ' 权重(1-10000)'" min-width="200" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="weight-cell-vertical">
|
||||||
|
<div class="weight-slider-wrap">
|
||||||
|
<el-slider
|
||||||
|
:model-value="getItemWeightForCurrentDirection(row)"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
size="small"
|
||||||
|
:disabled="isWeightDisabled(row, t)"
|
||||||
|
class="weight-slider"
|
||||||
|
@update:model-value="(v: number | number[]) => setItemWeightForCurrentDirection(t, row, Array.isArray(v) ? (v[0] ?? 1) : (v ?? 1))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="weight-input-wrap">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) <= 1"
|
||||||
|
@click="setItemWeightForCurrentDirection(t, row, Math.max(1, getItemWeightForCurrentDirection(row) - 1))"
|
||||||
|
>-</el-button>
|
||||||
|
<el-input-number
|
||||||
|
:model-value="getItemWeightForCurrentDirection(row)"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
:disabled="isWeightDisabled(row, t)"
|
||||||
|
controls-position="right"
|
||||||
|
size="small"
|
||||||
|
class="weight-input"
|
||||||
|
@update:model-value="(v: number | string | undefined) => setItemWeightForCurrentDirection(t, row, typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="isWeightDisabled(row, t) || getItemWeightForCurrentDirection(row) >= 10000"
|
||||||
|
@click="setItemWeightForCurrentDirection(t, row, Math.min(10000, getItemWeightForCurrentDirection(row) + 1))"
|
||||||
|
>+</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmit">提交</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import api from '../../../api/reward/index'
|
||||||
|
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
|
||||||
|
type DirectionKey = 'clockwise' | 'counterclockwise'
|
||||||
|
|
||||||
|
/** 当前方向下的行:reward_id=DiceReward 主键(用于提交更新),id=end_index 展示用,weight 为当前方向权重 */
|
||||||
|
interface WeightRow {
|
||||||
|
reward_id?: number
|
||||||
|
id?: number
|
||||||
|
grid_number?: number
|
||||||
|
real_ev?: number
|
||||||
|
ui_text?: string
|
||||||
|
remark?: string
|
||||||
|
weight: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按档位、方向分组:tier -> { '0': 顺时针行列表, '1': 逆时针行列表 } */
|
||||||
|
type GroupedByTierDirection = Record<string, { '0': WeightRow[]; '1': WeightRow[] }>
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', value: boolean): void
|
||||||
|
(e: 'success'): 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 activeDirection = ref<'0' | '1'>('0')
|
||||||
|
const activeTier = ref('T1')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const grouped = ref<GroupedByTierDirection>({
|
||||||
|
T1: { '0': [], '1': [] },
|
||||||
|
T2: { '0': [], '1': [] },
|
||||||
|
T3: { '0': [], '1': [] },
|
||||||
|
T4: { '0': [], '1': [] },
|
||||||
|
T5: { '0': [], '1': [] }
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentDirectionLabel = computed(() =>
|
||||||
|
activeDirection.value === '0' ? '顺时针' : '逆时针'
|
||||||
|
)
|
||||||
|
|
||||||
|
const tierKeys = TIER_KEYS
|
||||||
|
|
||||||
|
function getTierItems(tier: string): WeightRow[] {
|
||||||
|
const dir = activeDirection.value
|
||||||
|
const tierData = grouped.value[tier]
|
||||||
|
if (!tierData || !tierData[dir]) return []
|
||||||
|
return tierData[dir]
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTierChartLabels(tier: string): string[] {
|
||||||
|
return getTierItems(tier).map((r) => String(r.grid_number ?? ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTierChartDataForCurrentDirection(tier: string): number[] {
|
||||||
|
return getTierItems(tier).map((r) => toWeightPrecision(r.weight ?? 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTierSumForCurrentDirection(tier: string): number {
|
||||||
|
return getTierItems(tier).reduce((s, r) => s + toWeightPrecision(r.weight ?? 1), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemWeightForCurrentDirection(row: WeightRow): number {
|
||||||
|
const w = row.weight
|
||||||
|
const num = typeof w === 'number' && !Number.isNaN(w) ? w : Number(w)
|
||||||
|
if (Number.isNaN(num)) return 1
|
||||||
|
return toWeightPrecision(num)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toWeightPrecision(value: number): number {
|
||||||
|
return Math.max(1, Math.min(10000, Math.floor(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setItemWeightForCurrentDirection(tier: string, row: WeightRow, value: number) {
|
||||||
|
const v = toWeightPrecision(value)
|
||||||
|
const dir = activeDirection.value
|
||||||
|
const tierData = grouped.value[tier]
|
||||||
|
if (!tierData || !tierData[dir]) return
|
||||||
|
const list = tierData[dir]
|
||||||
|
const rid = row.reward_id != null ? row.reward_id : undefined
|
||||||
|
list.forEach((r) => {
|
||||||
|
if (r === row || (rid != null && r.reward_id != null && r.reward_id === rid)) r.weight = v
|
||||||
|
})
|
||||||
|
grouped.value[tier][dir] = [...list]
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWeightDisabled(row: WeightRow, tier: string): boolean {
|
||||||
|
if (tier === 'T4' || tier === 'T5') return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWeightValue(v: unknown): number {
|
||||||
|
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
||||||
|
if (Number.isNaN(num)) return 1
|
||||||
|
return Math.max(1, Math.min(10000, Math.floor(num)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 兼容后端返回 tier -> { 0: [], 1: [] },每行含 reward_id, id(end_index), grid_number, ui_text, real_ev, remark, weight */
|
||||||
|
function parsePayload(res: any): GroupedByTierDirection {
|
||||||
|
const empty: GroupedByTierDirection = {
|
||||||
|
T1: { '0': [], '1': [] },
|
||||||
|
T2: { '0': [], '1': [] },
|
||||||
|
T3: { '0': [], '1': [] },
|
||||||
|
T4: { '0': [], '1': [] },
|
||||||
|
T5: { '0': [], '1': [] }
|
||||||
|
}
|
||||||
|
if (!res || typeof res !== 'object') return empty
|
||||||
|
const raw = res?.data ?? res
|
||||||
|
if (!raw || typeof raw !== 'object') return empty
|
||||||
|
const out = { ...empty }
|
||||||
|
for (const t of TIER_KEYS) {
|
||||||
|
const tierData = raw[t]
|
||||||
|
if (!tierData || typeof tierData !== 'object') continue
|
||||||
|
out[t] = {
|
||||||
|
'0': Array.isArray(tierData[0])
|
||||||
|
? tierData[0].map((r: any) => ({
|
||||||
|
...r,
|
||||||
|
reward_id: r.reward_id != null ? Number(r.reward_id) : undefined,
|
||||||
|
weight: normalizeWeightValue(r.weight)
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
'1': Array.isArray(tierData[1])
|
||||||
|
? tierData[1].map((r: any) => ({
|
||||||
|
...r,
|
||||||
|
reward_id: r.reward_id != null ? Number(r.reward_id) : undefined,
|
||||||
|
weight: normalizeWeightValue(r.weight)
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
api
|
||||||
|
.weightRatioListWithDirection()
|
||||||
|
.then((res: any) => {
|
||||||
|
grouped.value = parsePayload(res)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
ElMessage.error('获取权重数据失败')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按 DiceReward 主键 id 收集:每条记录一条 { id, weight },直接用于后端按 id 更新 */
|
||||||
|
function collectItems(): Array<{ id: number; weight: number }> {
|
||||||
|
const items: Array<{ id: number; weight: number }> = []
|
||||||
|
for (const t of TIER_KEYS) {
|
||||||
|
const tierData = grouped.value[t]
|
||||||
|
if (!tierData) continue
|
||||||
|
for (const dir of ['0', '1'] as const) {
|
||||||
|
const list = tierData[dir] ?? []
|
||||||
|
for (const row of list) {
|
||||||
|
const rid = row.reward_id != null ? Number(row.reward_id) : 0
|
||||||
|
if (rid <= 0) continue
|
||||||
|
const w = isWeightDisabled(row, t) ? 10000 : toWeightPrecision(row.weight ?? 1)
|
||||||
|
items.push({ id: rid, weight: w })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
const items = collectItems()
|
||||||
|
if (items.length === 0) {
|
||||||
|
ElMessage.info('没有可提交的配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submitting.value = true
|
||||||
|
api
|
||||||
|
.batchUpdateWeights(items)
|
||||||
|
.then(() => {
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
emit('success')
|
||||||
|
handleClose()
|
||||||
|
})
|
||||||
|
.catch((e: { message?: string }) => {
|
||||||
|
ElMessage.error(e?.message ?? '保存失败')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitting.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(open) => {
|
||||||
|
if (open) {
|
||||||
|
loadData()
|
||||||
|
activeDirection.value = '0'
|
||||||
|
activeTier.value = 'T1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.direction-tabs {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.tier-tabs {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.chart-wrap {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.weight-sum {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.weight-sum-t4t5 {
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
.global-tip {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.weight-table {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.weight-cell-vertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.weight-slider-wrap {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 80px;
|
||||||
|
padding: 0 8px;
|
||||||
|
.weight-slider {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.weight-input-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.empty-tip {
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
.dialog-body {
|
||||||
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,11 +10,13 @@
|
|||||||
<ElSpace wrap>
|
<ElSpace wrap>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-permission="'dice:reward_config:index:update'"
|
v-permission="'dice:reward_config:index:update'"
|
||||||
type="primary"
|
type="warning"
|
||||||
@click="weightRatioVisible = true"
|
:loading="createRewardLoading"
|
||||||
|
@click="handleCreateRewardReference"
|
||||||
v-ripple
|
v-ripple
|
||||||
|
title="按规则:start_index=config(grid_number).id;顺时针 end_index=(start_index+grid_number)%26;逆时针 end_index=start_index-grid_number≥0?start_index-grid_number:26+start_index-grid_number"
|
||||||
>
|
>
|
||||||
T1-T5 与 BIGWIN 权重配比
|
创建奖励对照
|
||||||
</ElButton>
|
</ElButton>
|
||||||
<ElButton
|
<ElButton
|
||||||
v-permission="'dice:reward_config:index:index'"
|
v-permission="'dice:reward_config:index:index'"
|
||||||
@@ -66,24 +68,52 @@
|
|||||||
:data="dialogData"
|
:data="dialogData"
|
||||||
@success="refreshData"
|
@success="refreshData"
|
||||||
/>
|
/>
|
||||||
<!-- T1-T5、BIGWIN 权重配比弹窗 -->
|
|
||||||
<WeightRatioDialog v-model="weightRatioVisible" @success="refreshData" />
|
|
||||||
<!-- 权重配比测试弹窗 -->
|
<!-- 权重配比测试弹窗 -->
|
||||||
<WeightTestDialog v-model="weightTestVisible" />
|
<WeightTestDialog v-model="weightTestVisible" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { useTable } from '@/hooks/core/useTable'
|
import { useTable } from '@/hooks/core/useTable'
|
||||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||||
import api from '../../api/reward_config/index'
|
import api from '../../api/reward_config/index'
|
||||||
import TableSearch from './modules/table-search.vue'
|
import TableSearch from './modules/table-search.vue'
|
||||||
import EditDialog from './modules/edit-dialog.vue'
|
import EditDialog from './modules/edit-dialog.vue'
|
||||||
import WeightRatioDialog from './modules/weight-ratio-dialog.vue'
|
|
||||||
import WeightTestDialog from './modules/weight-test-dialog.vue'
|
import WeightTestDialog from './modules/weight-test-dialog.vue'
|
||||||
|
|
||||||
const weightRatioVisible = ref(false)
|
|
||||||
const weightTestVisible = ref(false)
|
const weightTestVisible = ref(false)
|
||||||
|
const createRewardLoading = ref(false)
|
||||||
|
|
||||||
|
async function handleCreateRewardReference() {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'按规则创建奖励对照:起始索引 start_index=奖励配置中 grid_number 对应格位的 id;顺时针 end_index=(start_index+摇取点数)%26;逆时针 end_index=start_index-摇取点数≥0 则取该值,否则 26+start_index-摇取点数。先清空现有数据再为 5-30 共 26 个点数、顺/逆时针分别生成。是否继续?',
|
||||||
|
'创建奖励对照',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定创建',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
createRewardLoading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await api.createRewardReference()
|
||||||
|
const data = res?.data ?? res
|
||||||
|
const msg =
|
||||||
|
typeof data === 'object' && data !== null
|
||||||
|
? `已按 5-30 共26个点数、顺时针+逆时针创建:顺时针新增 ${data.created_clockwise ?? 0} 条、逆时针新增 ${data.created_counterclockwise ?? 0} 条;顺时针更新 ${data.updated_clockwise ?? 0} 条、逆时针更新 ${data.updated_counterclockwise ?? 0} 条${(data.skipped ?? 0) > 0 ? `;${data.skipped} 个点数使用兜底起始索引` : ''}`
|
||||||
|
: '创建成功'
|
||||||
|
ElMessage.success(msg)
|
||||||
|
} catch (e: any) {
|
||||||
|
ElMessage.error(e?.message ?? '创建奖励对照失败')
|
||||||
|
} finally {
|
||||||
|
createRewardLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 搜索表单
|
// 搜索表单
|
||||||
const searchForm = ref<Record<string, unknown>>({
|
const searchForm = ref<Record<string, unknown>>({
|
||||||
@@ -121,12 +151,12 @@
|
|||||||
apiParams: { limit: 100 },
|
apiParams: { limit: 100 },
|
||||||
columnsFactory: () => [
|
columnsFactory: () => [
|
||||||
// { type: 'selection' },
|
// { type: 'selection' },
|
||||||
{ prop: 'id', label: 'ID(索引)', width: 80, align: 'center' },
|
|
||||||
{ prop: 'grid_number', label: '色子点数', align: 'center' },
|
{ prop: 'grid_number', label: '色子点数', align: 'center' },
|
||||||
{ prop: 'ui_text', label: '前端显示文本', align: 'center' },
|
{ prop: 'ui_text', label: '前端显示文本', align: 'center' },
|
||||||
{ prop: 'real_ev', label: '真实资金结算', align: 'center' },
|
{ prop: 'real_ev', label: '真实资金结算', align: 'center' },
|
||||||
{ prop: 'tier', label: '所属档位', sortable: true, align: 'center' },
|
{ prop: 'tier', label: '所属档位', sortable: true, align: 'center' },
|
||||||
{ prop: 'weight', label: '权重(1-10000)', width: 110, align: 'center' },
|
{ prop: 'weight', label: '权重(1-10000)', sortable: true, align: 'center' },
|
||||||
|
// 权重已迁移至「权重配比」弹窗(dice_reward 表,区分顺时针/逆时针)
|
||||||
// { prop: 'create_time', label: '创建时间', sortable: true, align: 'center' },
|
// { prop: 'create_time', label: '创建时间', sortable: true, align: 'center' },
|
||||||
{
|
{
|
||||||
prop: 'operation',
|
prop: 'operation',
|
||||||
|
|||||||
@@ -37,25 +37,22 @@
|
|||||||
<el-option label="BIGWIN(超级大奖)" value="BIGWIN" />
|
<el-option label="BIGWIN(超级大奖)" value="BIGWIN" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="权重(1-10000)" prop="weight">
|
<!-- BIGWIN 时可编辑权重:10000=100% 中奖,0=0% 中奖;点数 5、30 固定 100% 不可改 -->
|
||||||
|
<el-form-item v-if="formData.tier === 'BIGWIN'" label="大奖权重" prop="weight">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="formData.weight"
|
v-model="formData.weight"
|
||||||
:min="1"
|
:min="0"
|
||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="100"
|
||||||
:disabled="isWeightFixed10000"
|
placeholder="0~10000,10000=100%中奖"
|
||||||
placeholder="1-10000"
|
:disabled="isBigwinWeightDisabled"
|
||||||
/>
|
/>
|
||||||
<div v-if="isWeightFixed10000" class="weight-fixed-hint">
|
<div v-if="isBigwinWeightDisabled" class="form-tip">
|
||||||
色子点数 5、30 固定 10000,不可修改权重
|
点数 5、30 摇到必中大奖,权重固定 10000
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="form-tip">10000=100% 中奖,0=0% 中奖;仅对点数 10/15/20/25 生效</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="顺时针起始索引" prop="s_start_index">
|
<!-- 权重已迁移至「T1-T5 与 BIGWIN 权重配比」弹窗(dice_reward 表);BIGWIN 时本弹窗可编辑 weight;起始索引已迁移至 dice_reward.start_index -->
|
||||||
<el-input-number v-model="formData.s_start_index" :min="0" :step="1" placeholder="s_start_index" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="逆时针起始索引" prop="n_start_index">
|
|
||||||
<el-input-number v-model="formData.n_start_index" :min="0" :step="1" placeholder="n_start_index" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remark">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="formData.remark"
|
v-model="formData.remark"
|
||||||
@@ -108,38 +105,22 @@
|
|||||||
set: (value) => emit('update:modelValue', value)
|
set: (value) => emit('update:modelValue', value)
|
||||||
})
|
})
|
||||||
|
|
||||||
/** BIGWIN 且 grid_number 为 5 或 30 时权重固定为 10000(禁止手动调整) */
|
|
||||||
const isWeightFixed10000 = computed(
|
|
||||||
() =>
|
|
||||||
formData.tier === 'BIGWIN' &&
|
|
||||||
(formData.grid_number === 5 || formData.grid_number === 30)
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表单验证规则
|
* 表单验证规则(权重已迁移至权重配比弹窗)
|
||||||
*/
|
*/
|
||||||
const rules = reactive<FormRules>({
|
const rules = reactive<FormRules>({
|
||||||
grid_number: [{ required: true, message: '色子点数必需填写', trigger: 'blur' }],
|
grid_number: [{ required: true, message: '色子点数必需填写', trigger: 'blur' }],
|
||||||
ui_text: [{ required: true, message: '前端显示文本必需填写', trigger: 'blur' }],
|
ui_text: [{ required: true, message: '前端显示文本必需填写', trigger: 'blur' }],
|
||||||
real_ev: [{ required: true, message: '真实资金结算必需填写', trigger: 'blur' }],
|
real_ev: [{ required: true, message: '真实资金结算必需填写', trigger: 'blur' }],
|
||||||
tier: [{ required: true, message: '所属档位必需填写', trigger: 'blur' }],
|
tier: [{ required: true, message: '所属档位必需填写', trigger: 'blur' }],
|
||||||
weight: [
|
weight: [{ type: 'number', min: 0, max: 10000, message: '大奖权重 0~10000', trigger: 'blur' }]
|
||||||
{
|
|
||||||
validator: (_rule: unknown, value: number | null, callback: (e?: Error) => void) => {
|
|
||||||
const n = value != null ? Number(value) : NaN
|
|
||||||
if (Number.isNaN(n) || n < 1 || n > 10000) {
|
|
||||||
callback(new Error('权重必须为 1-10000'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
callback()
|
|
||||||
},
|
|
||||||
trigger: 'blur'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
n_start_index: [],
|
|
||||||
s_start_index: []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 点数 5、30 固定 100% 中大奖,权重不可改 */
|
||||||
|
const isBigwinWeightDisabled = computed(
|
||||||
|
() => formData.tier === 'BIGWIN' && [5, 30].includes(Number(formData.grid_number))
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始数据
|
* 初始数据
|
||||||
*/
|
*/
|
||||||
@@ -149,10 +130,8 @@
|
|||||||
ui_text: '',
|
ui_text: '',
|
||||||
real_ev: '',
|
real_ev: '',
|
||||||
tier: '',
|
tier: '',
|
||||||
weight: 1 as number,
|
remark: '',
|
||||||
n_start_index: 0 as number,
|
weight: 10000 as number
|
||||||
s_start_index: 0 as number,
|
|
||||||
remark: ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,19 +151,6 @@
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
/** 当 BIGWIN 且 grid_number 为 5 或 30 时,权重固定为 10000 便于展示 */
|
|
||||||
watch(
|
|
||||||
() => [formData.tier, formData.grid_number],
|
|
||||||
() => {
|
|
||||||
if (
|
|
||||||
formData.tier === 'BIGWIN' &&
|
|
||||||
(formData.grid_number === 5 || formData.grid_number === 30)
|
|
||||||
) {
|
|
||||||
formData.weight = 10000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化页面数据
|
* 初始化页面数据
|
||||||
*/
|
*/
|
||||||
@@ -203,7 +169,7 @@
|
|||||||
*/
|
*/
|
||||||
const initForm = () => {
|
const initForm = () => {
|
||||||
if (!props.data) return
|
if (!props.data) return
|
||||||
const numKeys = ['id', 'grid_number', 'real_ev', 'weight', 'n_start_index', 's_start_index']
|
const numKeys = ['id', 'grid_number', 'real_ev', 'weight']
|
||||||
for (const key of Object.keys(formData)) {
|
for (const key of Object.keys(formData)) {
|
||||||
if (!(key in props.data)) continue
|
if (!(key in props.data)) continue
|
||||||
const val = props.data[key]
|
const val = props.data[key]
|
||||||
@@ -211,11 +177,20 @@
|
|||||||
if (numKeys.includes(key)) {
|
if (numKeys.includes(key)) {
|
||||||
const numVal = Number(val)
|
const numVal = Number(val)
|
||||||
;(formData as Record<string, unknown>)[key] =
|
;(formData as Record<string, unknown>)[key] =
|
||||||
key === 'id' ? numVal || null : Number.isNaN(numVal) ? 0 : numVal
|
key === 'id'
|
||||||
|
? numVal || null
|
||||||
|
: Number.isNaN(numVal)
|
||||||
|
? key === 'weight'
|
||||||
|
? 10000
|
||||||
|
: 0
|
||||||
|
: numVal
|
||||||
} else {
|
} else {
|
||||||
;(formData as Record<string, unknown>)[key] = val ?? ''
|
;(formData as Record<string, unknown>)[key] = val ?? ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (formData.tier === 'BIGWIN' && (formData.weight === undefined || formData.weight === null)) {
|
||||||
|
formData.weight = [5, 30].includes(Number(formData.grid_number)) ? 10000 : 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,14 +208,13 @@
|
|||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
try {
|
try {
|
||||||
await formRef.value.validate()
|
await formRef.value.validate()
|
||||||
const payload = { ...formData }
|
const payload = { ...formData } as Record<string, unknown>
|
||||||
const w = Number(payload.weight)
|
if (formData.tier === 'BIGWIN') {
|
||||||
payload.weight = Number.isNaN(w) ? 1 : Math.max(1, Math.min(10000, w))
|
const w = Number(formData.weight)
|
||||||
if (payload.tier === 'BIGWIN' && (payload.grid_number === 5 || payload.grid_number === 30)) {
|
payload.weight = isBigwinWeightDisabled.value ? 10000 : Number.isNaN(w) ? 10000 : w
|
||||||
payload.weight = 10000
|
} else {
|
||||||
|
delete payload.weight
|
||||||
}
|
}
|
||||||
payload.n_start_index = Number(payload.n_start_index) || 0
|
|
||||||
payload.s_start_index = Number(payload.s_start_index) || 0
|
|
||||||
if (props.dialogType === 'add') {
|
if (props.dialogType === 'add') {
|
||||||
await api.save(payload)
|
await api.save(payload)
|
||||||
ElMessage.success('新增成功')
|
ElMessage.success('新增成功')
|
||||||
@@ -257,9 +231,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.weight-fixed-hint {
|
.form-tip {
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,83 +1,73 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="T1-T5 与 BIGWIN 权重配比"
|
title="T1-T5 权重配比(顺时针/逆时针)"
|
||||||
width="600px"
|
width="900px"
|
||||||
align-center
|
align-center
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
>
|
>
|
||||||
|
<div class="global-tip">
|
||||||
|
权重来自<strong>奖励对照表(dice_reward)</strong>,按<strong>结束索引(DiceRewardConfig.id)</strong>区分<strong>顺时针</strong>与<strong>逆时针</strong>两套权重;抽奖时按当前方向取对应权重。
|
||||||
|
</div>
|
||||||
<el-tabs v-model="activeTier" type="card">
|
<el-tabs v-model="activeTier" type="card">
|
||||||
<el-tab-pane v-for="t in tierKeys" :key="t" :label="t" :name="t">
|
<el-tab-pane v-for="t in tierKeys" :key="t" :label="t" :name="t">
|
||||||
<div v-if="getTierItems(t).length === 0" class="empty-tip"> 该档位暂无配置数据 </div>
|
<div v-if="getTierItems(t).length === 0" class="empty-tip"> 该档位暂无配置数据 </div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="chart-wrap">
|
<div class="chart-wrap" v-if="t !== 'T4' && t !== 'T5'">
|
||||||
|
<div class="chart-row">
|
||||||
<ArtBarChart
|
<ArtBarChart
|
||||||
x-axis-name="色子点数"
|
x-axis-name="结束索引"
|
||||||
:x-axis-data="getTierChartLabels(t)"
|
:x-axis-data="getTierChartLabels(t)"
|
||||||
:data="getTierChartData(t)"
|
:data="getTierChartData(t, 'clockwise')"
|
||||||
height="220px"
|
height="180px"
|
||||||
|
/>
|
||||||
|
<ArtBarChart
|
||||||
|
x-axis-name="结束索引"
|
||||||
|
:x-axis-data="getTierChartLabels(t)"
|
||||||
|
:data="getTierChartData(t, 'counterclockwise')"
|
||||||
|
height="180px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="weight-sum" v-if="t !== 'BIGWIN'">
|
|
||||||
当前档位权重合计:<strong>{{ getTierSumForValidation(t) }}</strong>
|
|
||||||
(各条权重 1-10000,档位内按权重比抽取 grid_number,和不限制)
|
|
||||||
</div>
|
</div>
|
||||||
<div class="weight-sum weight-sum-bigwin" v-else>
|
<div class="weight-sum" v-if="t !== 'T4' && t !== 'T5'">
|
||||||
BIGWIN 为豹子权重,每条权重 1-10000
|
当前档位权重合计(顺时针):<strong>{{ getTierSumForValidation(t, 'clockwise') }}</strong>
|
||||||
|
;逆时针:<strong>{{ getTierSumForValidation(t, 'counterclockwise') }}</strong>
|
||||||
|
(各条 1-10000,档位内按权重比抽取,和不限制)
|
||||||
|
</div>
|
||||||
|
<div class="weight-sum weight-sum-t4t5" v-else>
|
||||||
|
T4、T5 档位抽中时仅有一个结果,无需配置权重。
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
<el-table :data="getTierItems(t)" border size="small" class="weight-table">
|
||||||
<el-table-column label="色子点数" prop="grid_number" width="50" align="center" />
|
<el-table-column label="结束索引(id)" prop="id" width="90" align="center" show-overflow-tooltip />
|
||||||
<el-table-column
|
<el-table-column label="色子点数" prop="grid_number" width="80" align="center" />
|
||||||
label="实际中奖金额"
|
<el-table-column label="实际中奖金额" prop="real_ev" width="90" align="center" show-overflow-tooltip />
|
||||||
prop="real_ev"
|
<el-table-column label="显示文本" prop="ui_text" min-width="70" align="center" show-overflow-tooltip />
|
||||||
width="90"
|
<el-table-column label="备注" prop="remark" min-width="70" align="center" show-overflow-tooltip />
|
||||||
align="center"
|
<el-table-column label="顺时针权重(1-10000)" min-width="160" align="center">
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
label="显示文本"
|
|
||||||
prop="ui_text"
|
|
||||||
min-width="80"
|
|
||||||
align="center"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
label="备注"
|
|
||||||
prop="remark"
|
|
||||||
min-width="80"
|
|
||||||
align="center"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column label="权重(1-10000)" min-width="200" align="center">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="weight-cell-vertical">
|
<div class="weight-cell-vertical">
|
||||||
<div class="weight-slider-wrap">
|
<div class="weight-slider-wrap">
|
||||||
<el-slider
|
<el-slider
|
||||||
:model-value="getItemWeight(row)"
|
:model-value="getItemWeight(row, 'clockwise')"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="1"
|
||||||
size="small"
|
size="small"
|
||||||
:disabled="isWeightDisabled(row, t)"
|
:disabled="isWeightDisabled(row, t)"
|
||||||
class="weight-slider"
|
class="weight-slider"
|
||||||
@update:model-value="
|
@update:model-value="(v: number | number[]) => setItemWeightByRow(t, row, 'clockwise', Array.isArray(v) ? (v[0] ?? 1) : (v ?? 1))"
|
||||||
(v: number | number[]) => setItemWeightByRow(t, row, normalizeSliderValue(v))
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="weight-input-wrap">
|
<div class="weight-input-wrap">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
native-type="button"
|
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') <= 1"
|
||||||
:disabled="isWeightDisabled(row, t) || getItemWeight(row) <= 1"
|
@click="setItemWeightByRow(t, row, 'clockwise', Math.max(1, getItemWeight(row, 'clockwise') - 1))"
|
||||||
@click.prevent="adjustWeightByRow(t, row, -1)"
|
>-</el-button>
|
||||||
>
|
|
||||||
-
|
|
||||||
</el-button>
|
|
||||||
<el-input-number
|
<el-input-number
|
||||||
:model-value="getItemWeight(row)"
|
:model-value="getItemWeight(row, 'clockwise')"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="10000"
|
:max="10000"
|
||||||
:step="1"
|
:step="1"
|
||||||
@@ -85,17 +75,57 @@
|
|||||||
controls-position="right"
|
controls-position="right"
|
||||||
size="small"
|
size="small"
|
||||||
class="weight-input"
|
class="weight-input"
|
||||||
@update:model-value="(v: number | string | undefined) => setItemWeightByRow(t, row, typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
|
@update:model-value="(v: number | string | undefined) => setItemWeightByRow(t, row, 'clockwise', typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
|
||||||
/>
|
/>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
native-type="button"
|
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'clockwise') >= 10000"
|
||||||
:disabled="isWeightDisabled(row, t) || getItemWeight(row) >= 10000"
|
@click="setItemWeightByRow(t, row, 'clockwise', Math.min(10000, getItemWeight(row, 'clockwise') + 1))"
|
||||||
@click.prevent="adjustWeightByRow(t, row, 1)"
|
>+</el-button>
|
||||||
>
|
</div>
|
||||||
+
|
</div>
|
||||||
</el-button>
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="逆时针权重(1-10000)" min-width="160" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="weight-cell-vertical">
|
||||||
|
<div class="weight-slider-wrap">
|
||||||
|
<el-slider
|
||||||
|
:model-value="getItemWeight(row, 'counterclockwise')"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
size="small"
|
||||||
|
:disabled="isWeightDisabled(row, t)"
|
||||||
|
class="weight-slider"
|
||||||
|
@update:model-value="(v: number | number[]) => setItemWeightByRow(t, row, 'counterclockwise', Array.isArray(v) ? (v[0] ?? 1) : (v ?? 1))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="weight-input-wrap">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'counterclockwise') <= 1"
|
||||||
|
@click="setItemWeightByRow(t, row, 'counterclockwise', Math.max(1, getItemWeight(row, 'counterclockwise') - 1))"
|
||||||
|
>-</el-button>
|
||||||
|
<el-input-number
|
||||||
|
:model-value="getItemWeight(row, 'counterclockwise')"
|
||||||
|
:min="1"
|
||||||
|
:max="10000"
|
||||||
|
:step="1"
|
||||||
|
:disabled="isWeightDisabled(row, t)"
|
||||||
|
controls-position="right"
|
||||||
|
size="small"
|
||||||
|
class="weight-input"
|
||||||
|
@update:model-value="(v: number | string | undefined) => setItemWeightByRow(t, row, 'counterclockwise', typeof v === 'number' && !Number.isNaN(v) ? v : Number(v) || 1)"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
link
|
||||||
|
:disabled="isWeightDisabled(row, t) || getItemWeight(row, 'counterclockwise') >= 10000"
|
||||||
|
@click="setItemWeightByRow(t, row, 'counterclockwise', Math.min(10000, getItemWeight(row, 'counterclockwise') + 1))"
|
||||||
|
>+</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -116,7 +146,21 @@
|
|||||||
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
import ArtBarChart from '@/components/core/charts/art-bar-chart/index.vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'] as const
|
const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5'] as const
|
||||||
|
type DirectionKey = 'clockwise' | 'counterclockwise'
|
||||||
|
|
||||||
|
interface WeightRow {
|
||||||
|
reward_id_clockwise?: number
|
||||||
|
reward_id_counterclockwise?: number
|
||||||
|
id?: number
|
||||||
|
grid_number?: number
|
||||||
|
real_ev?: number
|
||||||
|
ui_text?: string
|
||||||
|
remark?: string
|
||||||
|
tier?: string
|
||||||
|
weight_clockwise: number
|
||||||
|
weight_counterclockwise: number
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
@@ -142,170 +186,176 @@
|
|||||||
const activeTier = ref('T1')
|
const activeTier = ref('T1')
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
|
||||||
/** 按档位分组的数据:{ T1: [...], T2: [...], ... },每项为可修改 weight 的副本 */
|
const grouped = ref<Record<string, WeightRow[]>>({
|
||||||
const grouped = ref<Record<string, Array<Record<string, unknown> & { weight: number }>>>({
|
|
||||||
T1: [],
|
T1: [],
|
||||||
T2: [],
|
T2: [],
|
||||||
T3: [],
|
T3: [],
|
||||||
T4: [],
|
T4: [],
|
||||||
T5: [],
|
T5: []
|
||||||
BIGWIN: []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function getTierItems(tier: string) {
|
function getTierItems(tier: string): WeightRow[] {
|
||||||
return grouped.value[tier] ?? []
|
return grouped.value[tier] ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 图表横坐标为色子点数(grid_number) */
|
|
||||||
function getTierChartLabels(tier: string): string[] {
|
function getTierChartLabels(tier: string): string[] {
|
||||||
const items = getTierItems(tier)
|
return getTierItems(tier).map((r) => String(r.grid_number ?? ''))
|
||||||
return items.map((r) => String(r.grid_number ?? ''))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTierChartData(tier: string): number[] {
|
function getTierChartData(tier: string, dir: DirectionKey): number[] {
|
||||||
const items = getTierItems(tier)
|
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||||
return items.map((r) => getItemWeight(r)).map((n) => Number(n))
|
return getTierItems(tier).map((r) => toWeightPrecision(r[key] ?? 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 档位权重和(仅展示用,不限制) */
|
function getTierSumForValidation(tier: string, dir: DirectionKey): number {
|
||||||
function getTierSumForValidation(tier: string): number {
|
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||||
const items = getTierItems(tier)
|
return getTierItems(tier).reduce((s, r) => s + toWeightPrecision(r[key] ?? 1), 0)
|
||||||
return items.reduce((s, r) => s + getItemWeight(r), 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 读取权重,1-10000 整数(兼容后端 int 返回为 number 或 string) */
|
function getItemWeight(row: WeightRow, dir: DirectionKey): number {
|
||||||
function getItemWeight(row: Record<string, unknown> & { weight?: number | string }): number {
|
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||||
const w = row.weight
|
const w = row[key]
|
||||||
const num = typeof w === 'number' && !Number.isNaN(w) ? w : Number(w)
|
const num = typeof w === 'number' && !Number.isNaN(w) ? w : Number(w)
|
||||||
if (Number.isNaN(num)) return 1
|
if (Number.isNaN(num)) return 1
|
||||||
const n = Math.max(1, Math.min(10000, Math.floor(num)))
|
return toWeightPrecision(num)
|
||||||
return n
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setItemWeight(row: Record<string, unknown> & { weight: number }, value: number) {
|
|
||||||
const v = toWeightPrecision(value)
|
|
||||||
row.weight = v
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 权重 1-10000 整数 */
|
|
||||||
function toWeightPrecision(value: number): number {
|
function toWeightPrecision(value: number): number {
|
||||||
const n = Math.max(1, Math.min(10000, Math.floor(value)))
|
const n = Math.max(1, Math.min(10000, Math.floor(value)))
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 按档位与行更新权重:原地修改 list 中对应项 */
|
function setItemWeightByRow(tier: string, row: WeightRow, dir: DirectionKey, value: number) {
|
||||||
function setItemWeightByRow(tier: string, row: Record<string, unknown> & { weight: number }, value: number) {
|
|
||||||
const v = toWeightPrecision(value)
|
const v = toWeightPrecision(value)
|
||||||
const list = grouped.value[tier]
|
const list = grouped.value[tier]
|
||||||
if (!list) return
|
if (!list) return
|
||||||
const idx = list.findIndex((r) => r === row || (r.id != null && row.id != null && r.id === row.id))
|
const rid = dir === 'clockwise' ? row.reward_id_clockwise : row.reward_id_counterclockwise
|
||||||
|
const idx = list.findIndex(
|
||||||
|
(r) =>
|
||||||
|
r === row ||
|
||||||
|
(rid != null && (dir === 'clockwise' ? r.reward_id_clockwise : r.reward_id_counterclockwise) === rid)
|
||||||
|
)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
list[idx].weight = v
|
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||||
|
list[idx][key] = v
|
||||||
} else {
|
} else {
|
||||||
row.weight = v
|
const key = dir === 'clockwise' ? 'weight_clockwise' : 'weight_counterclockwise'
|
||||||
|
row[key] = v
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function adjustWeightByRow(tier: string, row: Record<string, unknown> & { weight: number }, delta: number) {
|
function isWeightDisabled(row: WeightRow, tier: string): boolean {
|
||||||
const cur = getItemWeight(row)
|
if (tier === 'T4' || tier === 'T5') return true
|
||||||
setItemWeightByRow(tier, row, cur + delta)
|
|
||||||
}
|
|
||||||
|
|
||||||
function adjustWeight(row: Record<string, unknown> & { weight: number }, delta: number) {
|
|
||||||
const cur = getItemWeight(row)
|
|
||||||
setItemWeight(row, cur + delta)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** BIGWIN 的 grid_number=5、30 不可修改权重(固定 10000) */
|
|
||||||
function isWeightDisabled(row: Record<string, unknown>, tier: string): boolean {
|
|
||||||
if (tier === 'BIGWIN' && (row.grid_number === 5 || row.grid_number === 30)) return true
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeSliderValue(v: number | number[]): number {
|
/** 解析 tier -> { 0: [], 1: [] },按 grid_number 合并为每档位一行,含 reward_id 与双方向权重 */
|
||||||
if (Array.isArray(v)) return (v[0] ?? 0) as number
|
function parseWeightRatioPayload(res: any): Record<string, WeightRow[]> {
|
||||||
return (v ?? 0) as number
|
const empty: Record<string, WeightRow[]> = {
|
||||||
|
T1: [],
|
||||||
|
T2: [],
|
||||||
|
T3: [],
|
||||||
|
T4: [],
|
||||||
|
T5: []
|
||||||
|
}
|
||||||
|
if (!res || typeof res !== 'object') return empty
|
||||||
|
const raw = res?.data ?? res
|
||||||
|
if (!raw || typeof raw !== 'object') return empty
|
||||||
|
const out = { ...empty }
|
||||||
|
for (const t of TIER_KEYS) {
|
||||||
|
const tierData = raw[t]
|
||||||
|
if (!tierData || typeof tierData !== 'object') continue
|
||||||
|
const list0 = Array.isArray(tierData[0]) ? tierData[0] : []
|
||||||
|
const list1 = Array.isArray(tierData[1]) ? tierData[1] : []
|
||||||
|
const byGrid = new Map<
|
||||||
|
number,
|
||||||
|
{
|
||||||
|
reward_id_clockwise?: number
|
||||||
|
reward_id_counterclockwise?: number
|
||||||
|
id?: number
|
||||||
|
grid_number?: number
|
||||||
|
real_ev?: number
|
||||||
|
ui_text?: string
|
||||||
|
remark?: string
|
||||||
|
weight_clockwise: number
|
||||||
|
weight_counterclockwise: number
|
||||||
|
}
|
||||||
|
>()
|
||||||
|
for (const r of list0) {
|
||||||
|
const gn = r.grid_number != null ? Number(r.grid_number) : NaN
|
||||||
|
if (!Number.isNaN(gn)) {
|
||||||
|
byGrid.set(gn, {
|
||||||
|
reward_id_clockwise: r.reward_id != null ? Number(r.reward_id) : undefined,
|
||||||
|
id: r.id != null ? Number(r.id) : undefined,
|
||||||
|
grid_number: gn,
|
||||||
|
real_ev: r.real_ev != null ? Number(r.real_ev) : undefined,
|
||||||
|
ui_text: r.ui_text,
|
||||||
|
remark: r.remark,
|
||||||
|
weight_clockwise: normalizeWeightValue(r.weight),
|
||||||
|
weight_counterclockwise: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const r of list1) {
|
||||||
|
const gn = r.grid_number != null ? Number(r.grid_number) : NaN
|
||||||
|
if (!Number.isNaN(gn)) {
|
||||||
|
const cur = byGrid.get(gn)
|
||||||
|
if (cur) {
|
||||||
|
cur.reward_id_counterclockwise = r.reward_id != null ? Number(r.reward_id) : undefined
|
||||||
|
cur.weight_counterclockwise = normalizeWeightValue(r.weight)
|
||||||
|
} else {
|
||||||
|
byGrid.set(gn, {
|
||||||
|
reward_id_counterclockwise: r.reward_id != null ? Number(r.reward_id) : undefined,
|
||||||
|
id: r.id != null ? Number(r.id) : undefined,
|
||||||
|
grid_number: gn,
|
||||||
|
real_ev: r.real_ev != null ? Number(r.real_ev) : undefined,
|
||||||
|
ui_text: r.ui_text,
|
||||||
|
remark: r.remark,
|
||||||
|
weight_clockwise: 1,
|
||||||
|
weight_counterclockwise: normalizeWeightValue(r.weight)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[t] = Array.from(byGrid.values())
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function normalizeWeightValue(v: unknown): number {
|
||||||
* 从接口返回值中解析出按档位分组的对象。
|
const num = typeof v === 'number' && !Number.isNaN(v) ? v : Number(v)
|
||||||
* 兼容:1) 直接返回 { T1: [], T2: [], ... } 2) 包装在 data 中 { data: { T1: [], ... } } 3) 平铺数组按 tier 分组
|
if (Number.isNaN(num)) return 1
|
||||||
*/
|
return Math.max(1, Math.min(10000, Math.floor(num)))
|
||||||
function parseWeightRatioPayload(res: any): Record<string, Array<Record<string, unknown>>> {
|
|
||||||
if (!res || typeof res !== 'object') return {}
|
|
||||||
const hasTierKeys = (obj: any) =>
|
|
||||||
obj && typeof obj === 'object' && TIER_KEYS.some((k) => Array.isArray(obj[k]))
|
|
||||||
if (hasTierKeys(res)) return res as Record<string, Array<Record<string, unknown>>>
|
|
||||||
if (hasTierKeys(res?.data)) return res.data as Record<string, Array<Record<string, unknown>>>
|
|
||||||
if (hasTierKeys(res?.data?.data))
|
|
||||||
return res.data.data as Record<string, Array<Record<string, unknown>>>
|
|
||||||
if (Array.isArray(res)) {
|
|
||||||
return res.reduce(
|
|
||||||
(acc: Record<string, Array<Record<string, unknown>>>, r: Record<string, unknown>) => {
|
|
||||||
const t = (r.tier as string) || ''
|
|
||||||
if (t && TIER_KEYS.includes(t as (typeof TIER_KEYS)[number])) {
|
|
||||||
if (!acc[t]) acc[t] = []
|
|
||||||
acc[t].push(r)
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
},
|
|
||||||
{} as Record<string, Array<Record<string, unknown>>>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadData() {
|
function loadData() {
|
||||||
api
|
api
|
||||||
.weightRatioList()
|
.weightRatioList()
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
const data = parseWeightRatioPayload(res)
|
grouped.value = parseWeightRatioPayload(res)
|
||||||
const next: Record<string, Array<Record<string, unknown> & { weight: number }>> = {
|
|
||||||
T1: [],
|
|
||||||
T2: [],
|
|
||||||
T3: [],
|
|
||||||
T4: [],
|
|
||||||
T5: [],
|
|
||||||
BIGWIN: []
|
|
||||||
}
|
|
||||||
for (const t of TIER_KEYS) {
|
|
||||||
const list = Array.isArray(data[t]) ? data[t] : []
|
|
||||||
next[t] = list.map((r) => {
|
|
||||||
const raw =
|
|
||||||
typeof r.weight === 'number' && !Number.isNaN(r.weight) ? r.weight : Number(r.weight) || 1
|
|
||||||
return { ...r, weight: Math.max(1, Math.min(10000, Math.floor(raw))) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
grouped.value = next
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
ElMessage.error('获取权重配比数据失败')
|
ElMessage.error('获取权重配比数据失败')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateAll(): boolean {
|
/** 按 DiceReward 主键 id 收集:每条记录一条 { id, weight } */
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 收集所有档位的 id + weight(1-10000),BIGWIN 的 5/30 提交时固定为 10000。
|
|
||||||
*/
|
|
||||||
function collectItems(): Array<{ id: number; weight: number }> {
|
function collectItems(): Array<{ id: number; weight: number }> {
|
||||||
const byId = new Map<number, number>()
|
const items: Array<{ id: number; weight: number }> = []
|
||||||
for (const t of TIER_KEYS) {
|
for (const t of TIER_KEYS) {
|
||||||
for (const row of getTierItems(t)) {
|
for (const row of getTierItems(t)) {
|
||||||
const id = row.id != null ? Number(row.id) : 0
|
const w0 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'clockwise')
|
||||||
if (id >= 0 && !Number.isNaN(id)) {
|
const w1 = isWeightDisabled(row, t) ? 10000 : getItemWeight(row, 'counterclockwise')
|
||||||
const w = isWeightDisabled(row, t) ? 10000 : getItemWeight(row)
|
const rid0 = row.reward_id_clockwise != null ? row.reward_id_clockwise : 0
|
||||||
byId.set(id, w)
|
const rid1 = row.reward_id_counterclockwise != null ? row.reward_id_counterclockwise : 0
|
||||||
|
if (rid0 > 0) items.push({ id: rid0, weight: w0 })
|
||||||
|
if (rid1 > 0) items.push({ id: rid1, weight: w1 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return items
|
||||||
return Array.from(byId.entries()).map(([id, weight]) => ({ id, weight }))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (!validateAll()) return
|
|
||||||
const items = collectItems()
|
const items = collectItems()
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
ElMessage.info('没有可提交的配置')
|
ElMessage.info('没有可提交的配置')
|
||||||
@@ -346,16 +396,31 @@
|
|||||||
.chart-wrap {
|
.chart-wrap {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
.chart-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.weight-sum {
|
.weight-sum {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
.over {
|
|
||||||
color: var(--el-color-danger);
|
|
||||||
}
|
}
|
||||||
}
|
.weight-sum-t4t5 {
|
||||||
.weight-sum-bigwin {
|
|
||||||
color: var(--el-text-color-secondary);
|
color: var(--el-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
.global-tip {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 6px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
.weight-table {
|
.weight-table {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
@@ -367,7 +432,7 @@
|
|||||||
}
|
}
|
||||||
.weight-slider-wrap {
|
.weight-slider-wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 100px;
|
min-width: 80px;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
.weight-slider {
|
.weight-slider {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -377,9 +442,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
.weight-input {
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use app\api\util\ReturnCode;
|
|||||||
use app\dice\model\config\DiceConfig;
|
use app\dice\model\config\DiceConfig;
|
||||||
use app\dice\model\play_record\DicePlayRecord;
|
use app\dice\model\play_record\DicePlayRecord;
|
||||||
use app\dice\model\player\DicePlayer;
|
use app\dice\model\player\DicePlayer;
|
||||||
use app\dice\model\reward_config\DiceRewardConfig;
|
use app\dice\model\reward\DiceRewardConfig;
|
||||||
use app\api\controller\BaseController;
|
use app\api\controller\BaseController;
|
||||||
use app\api\util\ApiLang;
|
use app\api\util\ApiLang;
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ use app\dice\model\play_record\DicePlayRecord;
|
|||||||
use app\dice\model\player\DicePlayer;
|
use app\dice\model\player\DicePlayer;
|
||||||
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
|
||||||
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
use app\dice\model\player_wallet_record\DicePlayerWalletRecord;
|
||||||
use app\dice\model\reward_config\DiceRewardConfig;
|
use app\dice\model\reward\DiceReward;
|
||||||
|
use app\dice\model\reward\DiceRewardConfig;
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
use support\Log;
|
use support\Log;
|
||||||
use support\think\Cache;
|
use support\think\Cache;
|
||||||
@@ -85,7 +86,9 @@ class PlayStartLogic
|
|||||||
$safetyLine = (int) ($config->safety_line ?? 0);
|
$safetyLine = (int) ($config->safety_line ?? 0);
|
||||||
$usePoolWeights = $poolProfit >= $safetyLine;
|
$usePoolWeights = $poolProfit >= $safetyLine;
|
||||||
|
|
||||||
// 按档位 T1-T5 抽取后,直接按该档位内 weight 抽取一条 DiceRewardConfig,得到 grid_number
|
// 按档位 T1-T5 抽取后,从 DiceReward 表按当前方向取该档位数据,再按 weight 抽取一条得到 grid_number
|
||||||
|
$rewardInstance = DiceReward::getCachedInstance();
|
||||||
|
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
|
||||||
$maxTierRetry = 10;
|
$maxTierRetry = 10;
|
||||||
$chosen = null;
|
$chosen = null;
|
||||||
$tier = null;
|
$tier = null;
|
||||||
@@ -93,9 +96,9 @@ class PlayStartLogic
|
|||||||
$tier = $usePoolWeights
|
$tier = $usePoolWeights
|
||||||
? LotteryService::drawTierByWeights($config)
|
? LotteryService::drawTierByWeights($config)
|
||||||
: LotteryService::drawTierByPlayerWeights($player);
|
: LotteryService::drawTierByPlayerWeights($player);
|
||||||
$tierRewards = DiceRewardConfig::getCachedByTier($tier);
|
$tierRewards = $byTierDirection[$tier][$direction] ?? [];
|
||||||
if (empty($tierRewards)) {
|
if (empty($tierRewards)) {
|
||||||
Log::warning("档位 {$tier} 无任何奖励配置,重新摇取档位");
|
Log::warning("档位 {$tier} 方向 {$direction} 无任何 DiceReward,重新摇取档位");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -110,23 +113,18 @@ class PlayStartLogic
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if ($chosen === null) {
|
if ($chosen === null) {
|
||||||
Log::error("多次摇取档位后仍无有效权重配置");
|
Log::error("多次摇取档位后仍无有效 DiceReward");
|
||||||
throw new ApiException('暂无可用奖励配置');
|
throw new ApiException('暂无可用奖励配置');
|
||||||
}
|
}
|
||||||
$chosenId = (int) ($chosen['id'] ?? 0);
|
|
||||||
|
|
||||||
// 起始索引:根据 direction 取 s_start_index(顺时针)或 n_start_index(逆时针);结束索引:当前中奖配置 id
|
$startIndex = (int) ($chosen['start_index'] ?? 0);
|
||||||
$startIndex = $direction === 0
|
$targetIndex = (int) ($chosen['end_index'] ?? 0);
|
||||||
? (int) ($chosen['s_start_index'] ?? 0)
|
|
||||||
: (int) ($chosen['n_start_index'] ?? 0);
|
|
||||||
$targetIndex = $chosenId;
|
|
||||||
$rollNumber = (int) ($chosen['grid_number'] ?? 0);
|
$rollNumber = (int) ($chosen['grid_number'] ?? 0);
|
||||||
$realEv = (float) ($chosen['real_ev'] ?? 0);
|
$realEv = (float) ($chosen['real_ev'] ?? 0);
|
||||||
$isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5';
|
$isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5';
|
||||||
// T5(再来一次):摇色子中奖平台币 = 配置的 real_ev;其他档位 = 100 + real_ev
|
|
||||||
$rewardWinCoin = $isTierT5 ? $realEv : (100 + $realEv);
|
$rewardWinCoin = $isTierT5 ? $realEv : (100 + $realEv);
|
||||||
|
|
||||||
// 流程:先抽档位 T1-T5,再按档位内权重抽色子点数;5 和 30 抽到即豹子,10/15/20/25 按 BIGWIN weight 判定是否中大奖(豹子如 [4,4,4,4,4])
|
// 豹子判定:5/30 必豹子;10/15/20/25 按 DiceRewardConfig 中 BIGWIN 该点数的 weight 判定(0-10000,10000=100%)
|
||||||
$superWinCoin = 0;
|
$superWinCoin = 0;
|
||||||
$isWin = 0;
|
$isWin = 0;
|
||||||
$bigWinRealEv = 0.0;
|
$bigWinRealEv = 0.0;
|
||||||
@@ -135,21 +133,26 @@ class PlayStartLogic
|
|||||||
$alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true);
|
$alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true);
|
||||||
$doSuperWin = $alwaysSuperWin;
|
$doSuperWin = $alwaysSuperWin;
|
||||||
if (!$doSuperWin) {
|
if (!$doSuperWin) {
|
||||||
$weight = $bigWinConfig !== null
|
$bigWinWeight = 10000;
|
||||||
? max(1, min(10000, (int) ($bigWinConfig['weight'] ?? 1)))
|
if ($bigWinConfig !== null && isset($bigWinConfig['weight'])) {
|
||||||
: 10000;
|
$bigWinWeight = min(10000, max(0, (int) $bigWinConfig['weight']));
|
||||||
|
$bigWinRealEv = (float) ($bigWinConfig['real_ev'] ?? 0);
|
||||||
|
}
|
||||||
$roll = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX);
|
$roll = (random_int(0, PHP_INT_MAX - 1) / (float) PHP_INT_MAX);
|
||||||
$doSuperWin = $roll < ($weight / 10000.0);
|
$doSuperWin = $bigWinWeight > 0 && $roll < ($bigWinWeight / 10000.0);
|
||||||
|
} else {
|
||||||
|
if ($bigWinConfig !== null) {
|
||||||
|
$bigWinRealEv = (float) ($bigWinConfig['real_ev'] ?? 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ($doSuperWin) {
|
if ($doSuperWin) {
|
||||||
$rollArray = $this->getSuperWinRollArray($rollNumber);
|
$rollArray = $this->getSuperWinRollArray($rollNumber);
|
||||||
$isWin = 1;
|
$isWin = 1;
|
||||||
if ($bigWinConfig !== null) {
|
$superWinCoin = $bigWinRealEv > 0 ? $bigWinRealEv : self::SUPER_WIN_BONUS;
|
||||||
$bigWinRealEv = (float) ($bigWinConfig['real_ev'] ?? 0);
|
// 中 BIGWIN 豹子:不走原奖励流程,不记录原奖励,不触发 T5 再来一次,仅发放豹子奖金
|
||||||
$superWinCoin = $bigWinRealEv;
|
$rewardWinCoin = 0;
|
||||||
} else {
|
$realEv = 0;
|
||||||
$superWinCoin = self::SUPER_WIN_BONUS;
|
$isTierT5 = false;
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
|
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
|
||||||
}
|
}
|
||||||
@@ -164,11 +167,11 @@ class PlayStartLogic
|
|||||||
$startIndex,
|
$startIndex,
|
||||||
$targetIndex
|
$targetIndex
|
||||||
));
|
));
|
||||||
$winCoin = $superWinCoin + $rewardWinCoin; // 赢取平台币 = 中大奖 + 摇色子中奖
|
$winCoin = $superWinCoin + $rewardWinCoin; // 赢取平台币 = 中大奖 + 摇色子中奖(豹子时 rewardWinCoin 已为 0)
|
||||||
|
|
||||||
$record = null;
|
$record = null;
|
||||||
$configId = (int) $config->id;
|
$configId = (int) $config->id;
|
||||||
$rewardId = $chosenId;
|
$rewardId = ($isWin === 1 && $superWinCoin > 0) ? 0 : $targetIndex; // 中豹子不记录原奖励配置 id
|
||||||
$configName = (string) ($config->name ?? '');
|
$configName = (string) ($config->name ?? '');
|
||||||
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
|
$adminId = ($player->admin_id ?? null) ? (int) $player->admin_id : null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use app\dice\logic\play_record\DicePlayRecordLogic;
|
|||||||
use app\dice\validate\play_record\DicePlayRecordValidate;
|
use app\dice\validate\play_record\DicePlayRecordValidate;
|
||||||
use app\dice\model\player\DicePlayer;
|
use app\dice\model\player\DicePlayer;
|
||||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
use app\dice\model\reward_config\DiceRewardConfig;
|
use app\dice\model\reward\DiceRewardConfig;
|
||||||
use plugin\saiadmin\service\Permission;
|
use plugin\saiadmin\service\Permission;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
use support\Response;
|
use support\Response;
|
||||||
|
|||||||
120
server/app/dice/controller/reward/DiceRewardController.php
Normal file
120
server/app/dice/controller/reward/DiceRewardController.php
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | saiadmin [ saiadmin快速开发框架 ]
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
namespace app\dice\controller\reward;
|
||||||
|
|
||||||
|
use app\dice\logic\reward\DiceRewardLogic;
|
||||||
|
use app\dice\model\reward\DiceReward;
|
||||||
|
use plugin\saiadmin\basic\BaseController;
|
||||||
|
use plugin\saiadmin\service\Permission;
|
||||||
|
use support\Request;
|
||||||
|
use support\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 奖励对照控制器(dice_reward,按方向分页列表 + 权重编辑)
|
||||||
|
*/
|
||||||
|
class DiceRewardController extends BaseController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 分页列表,按 direction 区分顺时针(0)/逆时针(1)
|
||||||
|
* 参数:direction(必), tier(选), page, limit, orderField, orderType
|
||||||
|
*/
|
||||||
|
#[Permission('奖励对照列表', 'dice:reward:index:index')]
|
||||||
|
public function index(Request $request): Response
|
||||||
|
{
|
||||||
|
$direction = $request->input('direction', null);
|
||||||
|
if ($direction === null || $direction === '') {
|
||||||
|
return $this->fail('请传入 direction(0=顺时针 1=逆时针)');
|
||||||
|
}
|
||||||
|
$direction = (int) $direction;
|
||||||
|
if (!in_array($direction, [DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE], true)) {
|
||||||
|
return $this->fail('direction 必须为 0(顺时针)或 1(逆时针)');
|
||||||
|
}
|
||||||
|
$tier = $request->input('tier', '');
|
||||||
|
$page = (int) $request->input('page', 1);
|
||||||
|
$limit = (int) $request->input('limit', 10);
|
||||||
|
$orderField = $request->input('orderField', 'r.tier');
|
||||||
|
$orderType = $request->input('orderType', 'asc');
|
||||||
|
|
||||||
|
$logic = new DiceRewardLogic();
|
||||||
|
$data = $logic->getListWithConfig($direction, [
|
||||||
|
'tier' => $tier,
|
||||||
|
'orderField' => $orderField,
|
||||||
|
'orderType' => $orderType,
|
||||||
|
], $page, $limit);
|
||||||
|
return $this->success($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权重编辑弹窗:按档位分组获取当前方向的配置+权重(单方向,用于兼容)
|
||||||
|
* 参数:direction 0=顺时针 1=逆时针
|
||||||
|
*/
|
||||||
|
#[Permission('奖励对照列表', 'dice:reward:index:index')]
|
||||||
|
public function weightRatioList(Request $request): Response
|
||||||
|
{
|
||||||
|
$direction = (int) $request->input('direction', 0);
|
||||||
|
if (!in_array($direction, [DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE], true)) {
|
||||||
|
$direction = DiceReward::DIRECTION_CLOCKWISE;
|
||||||
|
}
|
||||||
|
$logic = new DiceRewardLogic();
|
||||||
|
$data = $logic->getListGroupedByTierForDirection($direction);
|
||||||
|
return $this->success($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权重编辑弹窗:按档位分组获取配置+顺时针/逆时针权重(dice_reward 双方向)
|
||||||
|
* 返回与 reward_config 权重配比一致结构,供奖励对照页弹窗同时编辑 direction=0/1
|
||||||
|
*/
|
||||||
|
#[Permission('奖励对照列表', 'dice:reward:index:index')]
|
||||||
|
public function weightRatioListWithDirection(Request $request): Response
|
||||||
|
{
|
||||||
|
$logic = new DiceRewardLogic();
|
||||||
|
$data = $logic->getListGroupedByTierWithDirection();
|
||||||
|
return $this->success($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权重编辑弹窗:按方向+点数批量更新权重(写入 dice_reward)
|
||||||
|
* 参数:items: [{ grid_number, weight_clockwise, weight_counterclockwise }, ...]
|
||||||
|
*/
|
||||||
|
#[Permission('奖励对照修改', 'dice:reward:index:update')]
|
||||||
|
public function batchUpdateWeights(Request $request): Response
|
||||||
|
{
|
||||||
|
$items = $request->post('items', []);
|
||||||
|
if (!is_array($items)) {
|
||||||
|
return $this->fail('参数 items 必须为数组');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$logic = new DiceRewardLogic();
|
||||||
|
$logic->batchUpdateWeights($items);
|
||||||
|
return $this->success('保存成功');
|
||||||
|
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||||
|
return $this->fail($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权重编辑弹窗:批量更新当前方向的权重(单方向,用于兼容)
|
||||||
|
* 参数:direction(必), items: [{ id, weight }, ...]
|
||||||
|
*/
|
||||||
|
#[Permission('奖励对照修改', 'dice:reward:index:update')]
|
||||||
|
public function batchUpdateWeightsByDirection(Request $request): Response
|
||||||
|
{
|
||||||
|
$direction = (int) $request->post('direction', 0);
|
||||||
|
if (!in_array($direction, [DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE], true)) {
|
||||||
|
return $this->fail('direction 必须为 0(顺时针)或 1(逆时针)');
|
||||||
|
}
|
||||||
|
$items = $request->post('items', []);
|
||||||
|
if (!is_array($items)) {
|
||||||
|
return $this->fail('参数 items 必须为数组');
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$logic = new DiceRewardLogic();
|
||||||
|
$logic->batchUpdateWeightsByDirection($direction, $items);
|
||||||
|
return $this->success('保存成功');
|
||||||
|
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||||
|
return $this->fail($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ namespace app\dice\controller\reward_config;
|
|||||||
|
|
||||||
use plugin\saiadmin\basic\BaseController;
|
use plugin\saiadmin\basic\BaseController;
|
||||||
use app\dice\logic\reward_config\DiceRewardConfigLogic;
|
use app\dice\logic\reward_config\DiceRewardConfigLogic;
|
||||||
|
use app\dice\logic\reward\DiceRewardLogic;
|
||||||
use app\dice\validate\reward_config\DiceRewardConfigValidate;
|
use app\dice\validate\reward_config\DiceRewardConfigValidate;
|
||||||
use plugin\saiadmin\service\Permission;
|
use plugin\saiadmin\service\Permission;
|
||||||
use support\Request;
|
use support\Request;
|
||||||
@@ -124,20 +125,21 @@ class DiceRewardConfigController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* T1-T5、BIGWIN 权重配比:按档位分组返回配置列表
|
* T1-T5、BIGWIN 权重配比:按档位分组返回配置列表(含顺时针/逆时针权重,来自 dice_reward)
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
#[Permission('奖励配置列表', 'dice:reward_config:index:index')]
|
#[Permission('奖励配置列表', 'dice:reward_config:index:index')]
|
||||||
public function weightRatioList(Request $request): Response
|
public function weightRatioList(Request $request): Response
|
||||||
{
|
{
|
||||||
$data = $this->logic->getListGroupedByTier();
|
$rewardLogic = new DiceRewardLogic();
|
||||||
|
$data = $rewardLogic->getListGroupedByTierWithDirection();
|
||||||
return $this->success($data);
|
return $this->success($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* T1-T5、BIGWIN 权重配比:批量更新权重(单条 weight 1-10000,各档位权重和不限制)
|
* T1-T5、BIGWIN 权重配比:按 DiceReward 主键 id 批量更新 weight(写入 dice_reward,修改后刷新缓存)
|
||||||
* 保存后 Logic 会重新实例化奖励配置表缓存(DiceRewardConfig::refreshCache)
|
* items: [ { id: DiceReward.id, weight: 1-10000 }, ... ]
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
@@ -149,13 +151,32 @@ class DiceRewardConfigController extends BaseController
|
|||||||
return $this->fail('参数 items 必须为数组');
|
return $this->fail('参数 items 必须为数组');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
$this->logic->batchUpdateWeights($items);
|
$rewardLogic = new DiceRewardLogic();
|
||||||
|
$rewardLogic->batchUpdateWeights($items);
|
||||||
return $this->success('保存成功');
|
return $this->success('保存成功');
|
||||||
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||||
return $this->fail($e->getMessage());
|
return $this->fail($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建奖励对照:按当前 dice_reward_config 为两种方向(顺时针0、逆时针1)生成所有色子可能对应的 dice_reward 记录
|
||||||
|
* 权重默认 1,可在「奖励对照」页的权重编辑弹窗中调整
|
||||||
|
* @param Request $request
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
#[Permission('奖励配置修改', 'dice:reward_config:index:update')]
|
||||||
|
public function createRewardReference(Request $request): Response
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$rewardLogic = new DiceRewardLogic();
|
||||||
|
$result = $rewardLogic->createRewardReferenceFromConfig();
|
||||||
|
return $this->success($result, '创建奖励对照成功');
|
||||||
|
} catch (\plugin\saiadmin\exception\ApiException $e) {
|
||||||
|
return $this->fail($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 权重配比测试:仅模拟落点统计,不创建游玩记录。按当前配置在内存中模拟 N 次抽奖,返回各 grid_number 落点次数,可选保存到 dice_reward_config_record。
|
* 权重配比测试:仅模拟落点统计,不创建游玩记录。按当前配置在内存中模拟 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 概率
|
* @param Request $request test_count: 100|500|1000, save_record: bool, lottery_config_id: int|null 奖池配置ID,用于设定 T1-T5 概率
|
||||||
|
|||||||
427
server/app/dice/logic/reward/DiceRewardLogic.php
Normal file
427
server/app/dice/logic/reward/DiceRewardLogic.php
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | saiadmin [ saiadmin快速开发框架 ]
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
namespace app\dice\logic\reward;
|
||||||
|
|
||||||
|
use app\dice\model\reward\DiceReward;
|
||||||
|
use app\dice\model\reward_config\DiceRewardConfig;
|
||||||
|
use plugin\saiadmin\exception\ApiException;
|
||||||
|
use support\think\Db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 奖励对照逻辑层(DiceReward)
|
||||||
|
* 权重 1-10000,区分顺时针/逆时针,修改后刷新 DiceReward 缓存
|
||||||
|
*/
|
||||||
|
class DiceRewardLogic
|
||||||
|
{
|
||||||
|
private const WEIGHT_MIN = 1;
|
||||||
|
private const WEIGHT_MAX = 10000;
|
||||||
|
|
||||||
|
/** 档位键 */
|
||||||
|
private const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页列表(按方向筛选,关联 dice_reward_config 展示 grid_number、ui_text、real_ev、remark)
|
||||||
|
* @param int $direction 0=顺时针 1=逆时针
|
||||||
|
* @param array{tier?: string, orderField?: string, orderType?: string} $where tier 档位筛选
|
||||||
|
* @param int $page
|
||||||
|
* @param int $limit
|
||||||
|
* @return array{total: int, per_page: int, current_page: int, data: array}
|
||||||
|
*/
|
||||||
|
public function getListWithConfig(int $direction, array $where, int $page = 1, int $limit = 10): array
|
||||||
|
{
|
||||||
|
$tier = isset($where['tier']) ? trim((string) $where['tier']) : '';
|
||||||
|
$orderField = isset($where['orderField']) && $where['orderField'] !== '' ? (string) $where['orderField'] : 'r.tier';
|
||||||
|
$orderType = isset($where['orderType']) && strtoupper((string) $where['orderType']) === 'DESC' ? 'desc' : 'asc';
|
||||||
|
|
||||||
|
$query = DiceReward::alias('r')
|
||||||
|
->where('r.direction', $direction)
|
||||||
|
->field('r.id,r.tier,r.direction,r.end_index,r.weight,r.grid_number,r.start_index,r.ui_text,r.real_ev,r.remark,r.type,r.create_time,r.update_time')
|
||||||
|
->order($orderField, $orderType)
|
||||||
|
->order('r.end_index', 'asc');
|
||||||
|
|
||||||
|
if ($tier !== '') {
|
||||||
|
$query->where('r.tier', $tier);
|
||||||
|
}
|
||||||
|
|
||||||
|
$paginator = $query->paginate($limit, false, ['page' => $page]);
|
||||||
|
$arr = $paginator->toArray();
|
||||||
|
$data = isset($arr['data']) ? $arr['data'] : $arr['records'] ?? [];
|
||||||
|
$total = (int) ($arr['total'] ?? 0);
|
||||||
|
$perPage = (int) ($arr['per_page'] ?? $limit);
|
||||||
|
$currentPage = (int) ($arr['current_page'] ?? $page);
|
||||||
|
foreach ($data as $i => $row) {
|
||||||
|
if (isset($row['id']) && $row['id'] !== '' && $row['id'] !== null) {
|
||||||
|
$data[$i]['id'] = (int) $row['id'];
|
||||||
|
} else {
|
||||||
|
$data[$i]['id'] = isset($row['end_index']) ? (int) $row['end_index'] : 0;
|
||||||
|
}
|
||||||
|
$data[$i]['start_index'] = isset($row['start_index']) && $row['start_index'] !== '' && $row['start_index'] !== null
|
||||||
|
? (int) $row['start_index']
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'total' => $total,
|
||||||
|
'per_page' => $perPage,
|
||||||
|
'current_page' => $currentPage,
|
||||||
|
'data' => $data,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按单方向批量更新权重(仅更新当前方向的 weight,并刷新缓存)
|
||||||
|
* @param int $direction 0=顺时针 1=逆时针
|
||||||
|
* @param array<int, array{id: int, weight: int}> $items id 为 end_index(DiceRewardConfig.id)
|
||||||
|
*/
|
||||||
|
public function batchUpdateWeightsByDirection(int $direction, array $items): void
|
||||||
|
{
|
||||||
|
if (empty($items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$id = isset($item['id']) ? (int) $item['id'] : 0;
|
||||||
|
$weight = isset($item['weight']) ? (int) $item['weight'] : self::WEIGHT_MIN;
|
||||||
|
if ($id <= 0) {
|
||||||
|
throw new ApiException('存在无效的配置ID');
|
||||||
|
}
|
||||||
|
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $weight));
|
||||||
|
|
||||||
|
$tier = DiceRewardConfig::where('id', $id)->value('tier');
|
||||||
|
if ($tier === null || $tier === '') {
|
||||||
|
throw new ApiException('配置ID ' . $id . ' 不存在或档位为空');
|
||||||
|
}
|
||||||
|
$tier = (string) $tier;
|
||||||
|
|
||||||
|
$affected = DiceReward::where('tier', $tier)->where('direction', $direction)->where('end_index', $id)->update(['weight' => $weight]);
|
||||||
|
if ($affected === 0) {
|
||||||
|
$m = new DiceReward();
|
||||||
|
$m->tier = $tier;
|
||||||
|
$m->direction = $direction;
|
||||||
|
$m->end_index = $id;
|
||||||
|
$m->weight = $weight;
|
||||||
|
$m->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DiceReward::refreshCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按档位+单方向返回列表(用于权重编辑弹窗:当前方向下按档位分组的配置+权重)
|
||||||
|
* @param int $direction 0=顺时针 1=逆时针
|
||||||
|
* @return array<string, array> 键 T1|T2|...|BIGWIN,值为该档位下带 weight 的行数组
|
||||||
|
*/
|
||||||
|
public function getListGroupedByTierForDirection(int $direction): array
|
||||||
|
{
|
||||||
|
$configInstance = DiceRewardConfig::getCachedInstance();
|
||||||
|
$byTier = $configInstance['by_tier'] ?? [];
|
||||||
|
$rewardInstance = DiceReward::getCachedInstance();
|
||||||
|
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach (self::TIER_KEYS as $tier) {
|
||||||
|
$result[$tier] = [];
|
||||||
|
$rows = $byTier[$tier] ?? [];
|
||||||
|
$dirRows = $byTierDirection[$tier][$direction] ?? [];
|
||||||
|
$weightMap = [];
|
||||||
|
foreach ($dirRows as $r) {
|
||||||
|
$eid = isset($r['end_index']) ? (int) $r['end_index'] : 0;
|
||||||
|
$weightMap[$eid] = isset($r['weight']) ? max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $r['weight'])) : 1;
|
||||||
|
}
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$id = isset($row['id']) ? (int) $row['id'] : 0;
|
||||||
|
$result[$tier][] = [
|
||||||
|
'id' => $id,
|
||||||
|
'grid_number' => $row['grid_number'] ?? 0,
|
||||||
|
'ui_text' => $row['ui_text'] ?? '',
|
||||||
|
'real_ev' => $row['real_ev'] ?? 0,
|
||||||
|
'remark' => $row['remark'] ?? '',
|
||||||
|
'tier' => $tier,
|
||||||
|
'weight' => $weightMap[$id] ?? 1,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按档位+方向返回 DiceReward 列表(用于权重配比弹窗),直接读 dice_reward 表,不依赖 config
|
||||||
|
* 每行含 reward_id(DiceReward 主键,用于按 id 更新权重)、id(end_index 展示用)、grid_number、ui_text、real_ev、remark、weight
|
||||||
|
*
|
||||||
|
* @return array<string, array{0: array, 1: array}>
|
||||||
|
*/
|
||||||
|
public function getListGroupedByTierWithDirection(): array
|
||||||
|
{
|
||||||
|
$rewardInstance = DiceReward::getCachedInstance();
|
||||||
|
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach (self::TIER_KEYS as $tier) {
|
||||||
|
$result[$tier] = [0 => [], 1 => []];
|
||||||
|
foreach ([0, 1] as $direction) {
|
||||||
|
$rows = $byTierDirection[$tier][$direction] ?? [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$result[$tier][$direction][] = [
|
||||||
|
'reward_id' => isset($r['id']) ? (int) $r['id'] : 0,
|
||||||
|
'id' => isset($r['end_index']) ? (int) $r['end_index'] : 0,
|
||||||
|
'grid_number' => isset($r['grid_number']) ? (int) $r['grid_number'] : 0,
|
||||||
|
'ui_text' => (string) ($r['ui_text'] ?? ''),
|
||||||
|
'real_ev' => $r['real_ev'] ?? 0,
|
||||||
|
'remark' => (string) ($r['remark'] ?? ''),
|
||||||
|
'weight' => isset($r['weight']) ? max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $r['weight'])) : self::WEIGHT_MIN,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新权重:直接按 DiceReward 主键 id 更新 weight,不依赖 direction/grid_number
|
||||||
|
*
|
||||||
|
* @param array<int, array{id: int, weight: int}> $items 每项 id 为 dice_reward 表主键,weight 为 1-10000
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public function batchUpdateWeights(array $items): void
|
||||||
|
{
|
||||||
|
if (empty($items)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$id = isset($item['id']) ? (int) $item['id'] : 0;
|
||||||
|
$weight = isset($item['weight']) ? (int) $item['weight'] : self::WEIGHT_MIN;
|
||||||
|
if ($id <= 0) {
|
||||||
|
throw new ApiException('存在无效的 DiceReward id');
|
||||||
|
}
|
||||||
|
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $weight));
|
||||||
|
DiceReward::where('id', $id)->update(['weight' => $weight]);
|
||||||
|
}
|
||||||
|
DiceReward::refreshCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** BIGWIN 权重范围:0=0% 中奖,10000=100% 中奖;grid_number=5/30 固定 100% 不可改 */
|
||||||
|
private const BIGWIN_WEIGHT_MAX = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 grid_number 获取 BIGWIN 档位权重(取顺时针方向,用于编辑展示)
|
||||||
|
* 若 DiceReward 无该点数则 5/30 返回 10000,其余返回 0
|
||||||
|
*/
|
||||||
|
public function getBigwinWeightByGridNumber(int $gridNumber): int
|
||||||
|
{
|
||||||
|
$inst = DiceReward::getCachedInstance();
|
||||||
|
$rows = $inst['by_tier_direction']['BIGWIN'][DiceReward::DIRECTION_CLOCKWISE] ?? [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
if ((int) ($row['grid_number'] ?? 0) === $gridNumber) {
|
||||||
|
return min(self::BIGWIN_WEIGHT_MAX, (int) ($row['weight'] ?? self::BIGWIN_WEIGHT_MAX));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return in_array($gridNumber, [5, 30], true) ? self::BIGWIN_WEIGHT_MAX : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新 BIGWIN 档位某点数的权重(顺/逆时针同时更新);0=0% 中奖,10000=100% 中奖
|
||||||
|
*/
|
||||||
|
public function updateBigwinWeight(int $gridNumber, int $weight): void
|
||||||
|
{
|
||||||
|
$weight = min(self::BIGWIN_WEIGHT_MAX, max(0, $weight));
|
||||||
|
foreach ([DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE] as $direction) {
|
||||||
|
// 优先更新已存在记录
|
||||||
|
$affected = DiceReward::where('tier', 'BIGWIN')
|
||||||
|
->where('direction', $direction)
|
||||||
|
->where('grid_number', $gridNumber)
|
||||||
|
->update(['weight' => $weight]);
|
||||||
|
|
||||||
|
// 若不存在 BIGWIN 记录,则按当前 BIGWIN 配置懒加载创建一条 dice_reward 记录
|
||||||
|
if ($affected === 0) {
|
||||||
|
$config = DiceRewardConfig::where('tier', 'BIGWIN')
|
||||||
|
->where('grid_number', $gridNumber)
|
||||||
|
->find();
|
||||||
|
if ($config) {
|
||||||
|
$m = new DiceReward();
|
||||||
|
$m->tier = 'BIGWIN';
|
||||||
|
$m->direction = $direction;
|
||||||
|
$m->grid_number = (int) $gridNumber;
|
||||||
|
// 对于 BIGWIN,仅需保证 real_ev、weight、grid_number,start_index/end_index 取当前配置 id 即可
|
||||||
|
$m->start_index = (int) $config->id;
|
||||||
|
$m->end_index = (int) $config->id;
|
||||||
|
$m->ui_text = (string) ($config->ui_text ?? '');
|
||||||
|
$m->real_ev = (float) ($config->real_ev ?? 0);
|
||||||
|
$m->remark = (string) ($config->remark ?? '');
|
||||||
|
$m->type = $config->type ?? null;
|
||||||
|
$m->weight = $weight > 0 ? $weight : self::WEIGHT_MIN;
|
||||||
|
$m->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DiceReward::refreshCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 盘面格数(用于顺时针/逆时针计算 end_index) */
|
||||||
|
private const BOARD_SIZE = 26;
|
||||||
|
|
||||||
|
/** 点数摇取范围:5-30,顺时针与逆时针均需创建 */
|
||||||
|
private const GRID_NUMBER_MIN = 5;
|
||||||
|
private const GRID_NUMBER_MAX = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建奖励对照:先清空 dice_reward 表,再按两种方向为点数 5-30 生成记录。
|
||||||
|
*
|
||||||
|
* DiceReward 记录数据规则(与 config 通过 end_index 关联):
|
||||||
|
* - 方向:direction = 0(顺时针)/ 1(逆时针)
|
||||||
|
* - 摇取点数:grid_number
|
||||||
|
* - 起始索引:start_index = DiceRewardConfig::where('grid_number', $grid_number)->first()->id
|
||||||
|
* - 结束索引(顺时针):end_index = ($start_index + $grid_number) % 26(对 26 取余)
|
||||||
|
* - 结束索引(逆时针):end_index = ($start_index - $grid_number >= 0) ? ($start_index - $grid_number) : (26 + $start_index - $grid_number)
|
||||||
|
* - 奖励档位:tier = DiceRewardConfig::where('id', $end_index)->first()->tier
|
||||||
|
* - 显示ui:ui_text = DiceRewardConfig::where('id', $end_index)->first()->ui_text
|
||||||
|
* - 实际中奖:real_ev = DiceRewardConfig::where('id', $end_index)->first()->real_ev
|
||||||
|
* - 备注:remark = DiceRewardConfig::where('id', $end_index)->first()->remark
|
||||||
|
* - 类型:type = DiceRewardConfig::where('id', $end_index)->first()->type(-2=唯一惩罚,-1=抽水,0=回本,1=再来一次,2=小赚,3=大奖格)
|
||||||
|
* - weight 默认 1,后续在权重编辑弹窗设置
|
||||||
|
*
|
||||||
|
* 例如顺时针摇取点数为 5 时:start_index = 配置中 grid_number=5 对应格位的 id,
|
||||||
|
* 结束位置 = (起始位置 + grid_number) % 26,再取该位置的 config 的 id 作为 end_index。
|
||||||
|
* 使用「按 id 排序后的盘面位置 0-25」做环形计算,避免 config.id 非连续时取模结果找不到;
|
||||||
|
* 唯一键为 (direction, grid_number),保证每个点数、每个方向各一条记录,不因 end_index 相同而覆盖。
|
||||||
|
*
|
||||||
|
* @return array{created_clockwise: int, created_counterclockwise: int, updated_clockwise: int, updated_counterclockwise: int, skipped: int}
|
||||||
|
* @throws ApiException
|
||||||
|
*/
|
||||||
|
public function createRewardReferenceFromConfig(): array
|
||||||
|
{
|
||||||
|
$list = DiceRewardConfig::order('id', 'asc')->select()->toArray();
|
||||||
|
if (empty($list)) {
|
||||||
|
throw new ApiException('奖励配置为空,请先维护 dice_reward_config');
|
||||||
|
}
|
||||||
|
$configCount = count($list);
|
||||||
|
if ($configCount < self::BOARD_SIZE) {
|
||||||
|
throw new ApiException(
|
||||||
|
'奖励配置需覆盖 26 个格位(id 0-25 或 1-26),当前仅 ' . $configCount . ' 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table = (new DiceReward())->getTable();
|
||||||
|
Db::execute('DELETE FROM `' . $table . '`');
|
||||||
|
DiceReward::refreshCache();
|
||||||
|
|
||||||
|
// 按 id 排序后,盘面位置 0..25 对应 $list[$pos],避免 config.id 非 0-25/1-26 时取模结果找不到
|
||||||
|
$gridToPosition = [];
|
||||||
|
foreach ($list as $pos => $row) {
|
||||||
|
$gn = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
|
||||||
|
if ($gn >= self::GRID_NUMBER_MIN && $gn <= self::GRID_NUMBER_MAX && !isset($gridToPosition[$gn])) {
|
||||||
|
$gridToPosition[$gn] = $pos;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$createdCw = 0;
|
||||||
|
$createdCcw = 0;
|
||||||
|
$updatedCw = 0;
|
||||||
|
$updatedCcw = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
|
||||||
|
for ($gridNumber = self::GRID_NUMBER_MIN; $gridNumber <= self::GRID_NUMBER_MAX; $gridNumber++) {
|
||||||
|
if (!isset($gridToPosition[$gridNumber])) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$startPos = $gridToPosition[$gridNumber];
|
||||||
|
$startRow = $list[$startPos];
|
||||||
|
$startId = isset($startRow['id']) ? (int) $startRow['id'] : 0;
|
||||||
|
|
||||||
|
$endPosCw = ($startPos + $gridNumber) % self::BOARD_SIZE;
|
||||||
|
$endPosCcw = $startPos - $gridNumber >= 0 ? $startPos - $gridNumber : self::BOARD_SIZE + $startPos - $gridNumber;
|
||||||
|
|
||||||
|
$configCw = $list[$endPosCw] ?? null;
|
||||||
|
$configCcw = $list[$endPosCcw] ?? null;
|
||||||
|
$endIdCw = $configCw !== null && isset($configCw['id']) ? (int) $configCw['id'] : 0;
|
||||||
|
$endIdCcw = $configCcw !== null && isset($configCcw['id']) ? (int) $configCcw['id'] : 0;
|
||||||
|
|
||||||
|
if ($configCw !== null) {
|
||||||
|
$tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
|
||||||
|
if ($tier !== '') {
|
||||||
|
$payloadCw = [
|
||||||
|
'tier' => $tier,
|
||||||
|
'weight' => self::WEIGHT_MIN,
|
||||||
|
'grid_number' => $gridNumber,
|
||||||
|
'start_index' => $startId,
|
||||||
|
'end_index' => $endIdCw,
|
||||||
|
'ui_text' => $configCw['ui_text'] ?? '',
|
||||||
|
'real_ev' => $configCw['real_ev'] ?? null,
|
||||||
|
'remark' => $configCw['remark'] ?? '',
|
||||||
|
'type' => isset($configCw['type']) ? (int) $configCw['type'] : 0,
|
||||||
|
];
|
||||||
|
$existing = DiceReward::where('direction', DiceReward::DIRECTION_CLOCKWISE)->where('grid_number', $gridNumber)->find();
|
||||||
|
if ($existing) {
|
||||||
|
DiceReward::where('id', $existing->id)->update($payloadCw);
|
||||||
|
$updatedCw++;
|
||||||
|
} else {
|
||||||
|
$m = new DiceReward();
|
||||||
|
$m->tier = $tier;
|
||||||
|
$m->direction = DiceReward::DIRECTION_CLOCKWISE;
|
||||||
|
$m->end_index = $endIdCw;
|
||||||
|
$m->weight = self::WEIGHT_MIN;
|
||||||
|
$m->grid_number = $gridNumber;
|
||||||
|
$m->start_index = $startId;
|
||||||
|
$m->ui_text = $configCw['ui_text'] ?? '';
|
||||||
|
$m->real_ev = $configCw['real_ev'] ?? null;
|
||||||
|
$m->remark = $configCw['remark'] ?? '';
|
||||||
|
$m->type = isset($configCw['type']) ? (int) $configCw['type'] : 0;
|
||||||
|
$m->save();
|
||||||
|
$createdCw++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($configCcw !== null) {
|
||||||
|
$tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
|
||||||
|
if ($tier !== '') {
|
||||||
|
$payloadCcw = [
|
||||||
|
'tier' => $tier,
|
||||||
|
'weight' => self::WEIGHT_MIN,
|
||||||
|
'grid_number' => $gridNumber,
|
||||||
|
'start_index' => $startId,
|
||||||
|
'end_index' => $endIdCcw,
|
||||||
|
'ui_text' => $configCcw['ui_text'] ?? '',
|
||||||
|
'real_ev' => $configCcw['real_ev'] ?? null,
|
||||||
|
'remark' => $configCcw['remark'] ?? '',
|
||||||
|
'type' => isset($configCcw['type']) ? (int) $configCcw['type'] : 0,
|
||||||
|
];
|
||||||
|
$existing = DiceReward::where('direction', DiceReward::DIRECTION_COUNTERCLOCKWISE)->where('grid_number', $gridNumber)->find();
|
||||||
|
if ($existing) {
|
||||||
|
DiceReward::where('id', $existing->id)->update($payloadCcw);
|
||||||
|
$updatedCcw++;
|
||||||
|
} else {
|
||||||
|
$m = new DiceReward();
|
||||||
|
$m->tier = $tier;
|
||||||
|
$m->direction = DiceReward::DIRECTION_COUNTERCLOCKWISE;
|
||||||
|
$m->end_index = $endIdCcw;
|
||||||
|
$m->weight = self::WEIGHT_MIN;
|
||||||
|
$m->grid_number = $gridNumber;
|
||||||
|
$m->start_index = $startId;
|
||||||
|
$m->ui_text = $configCcw['ui_text'] ?? '';
|
||||||
|
$m->real_ev = $configCcw['real_ev'] ?? null;
|
||||||
|
$m->remark = $configCcw['remark'] ?? '';
|
||||||
|
$m->type = isset($configCcw['type']) ? (int) $configCcw['type'] : 0;
|
||||||
|
$m->save();
|
||||||
|
$createdCcw++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DiceReward::refreshCache();
|
||||||
|
return [
|
||||||
|
'created_clockwise' => $createdCw,
|
||||||
|
'created_counterclockwise' => $createdCcw,
|
||||||
|
'updated_clockwise' => $updatedCw,
|
||||||
|
'updated_counterclockwise' => $updatedCcw,
|
||||||
|
'skipped' => $skipped,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,10 @@
|
|||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
namespace app\dice\logic\reward_config;
|
namespace app\dice\logic\reward_config;
|
||||||
|
|
||||||
|
use app\dice\logic\reward\DiceRewardLogic;
|
||||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
use app\dice\model\reward_config\DiceRewardConfig;
|
use app\dice\model\reward\DiceRewardConfig;
|
||||||
use app\dice\model\reward_config\DiceRewardConfigRecord;
|
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||||
use plugin\saiadmin\basic\think\BaseLogic;
|
use plugin\saiadmin\basic\think\BaseLogic;
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
use plugin\saiadmin\utils\Helper;
|
use plugin\saiadmin\utils\Helper;
|
||||||
@@ -30,27 +31,43 @@ class DiceRewardConfigLogic extends BaseLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新增:weight 限制 1-10000;保存后刷新缓存
|
* 新增:保存后刷新缓存(权重已迁移至 dice_reward 表)
|
||||||
*/
|
*/
|
||||||
public function add(array $data): mixed
|
public function add(array $data): mixed
|
||||||
{
|
{
|
||||||
$data = $this->normalizeWeight($data);
|
|
||||||
$result = parent::add($data);
|
$result = parent::add($data);
|
||||||
DiceRewardConfig::refreshCache();
|
DiceRewardConfig::refreshCache();
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修改:weight 限制 1-10000;保存后刷新缓存
|
* 修改:保存后刷新缓存;BIGWIN 的 weight 直接写入 dice_reward_config 表,抽奖时从 Config 读取
|
||||||
*/
|
*/
|
||||||
public function edit($id, array $data): mixed
|
public function edit($id, array $data): mixed
|
||||||
{
|
{
|
||||||
$data = $this->normalizeWeight($data);
|
|
||||||
$result = parent::edit($id, $data);
|
$result = parent::edit($id, $data);
|
||||||
DiceRewardConfig::refreshCache();
|
DiceRewardConfig::refreshCache();
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为列表/分页数据中的 BIGWIN 行附加 weight(来自 DiceReward 缓存)
|
||||||
|
*/
|
||||||
|
public function enrichBigwinWeight(array $listResult): array
|
||||||
|
{
|
||||||
|
$key = isset($listResult['data']) ? 'data' : (isset($listResult['records']) ? 'records' : null);
|
||||||
|
if ($key === null || empty($listResult[$key])) {
|
||||||
|
return $listResult;
|
||||||
|
}
|
||||||
|
$rewardLogic = new DiceRewardLogic();
|
||||||
|
foreach ($listResult[$key] as $i => $row) {
|
||||||
|
if (isset($row['tier']) && $row['tier'] === 'BIGWIN' && isset($row['grid_number'])) {
|
||||||
|
$listResult[$key][$i]['weight'] = $rewardLogic->getBigwinWeightByGridNumber((int) $row['grid_number']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $listResult;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除后刷新缓存
|
* 删除后刷新缓存
|
||||||
*/
|
*/
|
||||||
@@ -64,18 +81,7 @@ class DiceRewardConfigLogic extends BaseLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* weight 限制 1-10000
|
* 按档位分组返回奖励配置列表(仅配置,权重在 dice_reward 表;权重配比请用 DiceRewardLogic::getListGroupedByTierWithDirection)
|
||||||
*/
|
|
||||||
private function normalizeWeight(array $data): array
|
|
||||||
{
|
|
||||||
$w = isset($data['weight']) ? (int) $data['weight'] : self::WEIGHT_MIN;
|
|
||||||
$data['weight'] = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $w));
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 按档位分组返回奖励配置列表(用于 T1-T5、BIGWIN 权重配比)
|
|
||||||
* @return array<string, array> 键为 T1|T2|T3|T4|T5|BIGWIN,值为该档位下的配置行数组
|
|
||||||
*/
|
*/
|
||||||
public function getListGroupedByTier(): array
|
public function getListGroupedByTier(): array
|
||||||
{
|
{
|
||||||
@@ -94,50 +100,6 @@ class DiceRewardConfigLogic extends BaseLogic
|
|||||||
return $grouped;
|
return $grouped;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量更新权重:单条 weight 1-10000,各档位权重和不限制
|
|
||||||
* @param array<int, array{id: int, weight: int}> $items 元素为 [ id => 配置ID, weight => 1-10000 ]
|
|
||||||
* @throws ApiException 当单条 weight 非法时
|
|
||||||
*/
|
|
||||||
public function batchUpdateWeights(array $items): void
|
|
||||||
{
|
|
||||||
if (empty($items)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$items = array_values($items);
|
|
||||||
$ids = [];
|
|
||||||
$weightById = [];
|
|
||||||
foreach ($items as $item) {
|
|
||||||
if (!is_array($item)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$id = isset($item['id']) ? (int) $item['id'] : 0;
|
|
||||||
$w = isset($item['weight']) ? (int) $item['weight'] : self::WEIGHT_MIN;
|
|
||||||
if ($id < 0) {
|
|
||||||
throw new ApiException('存在无效的配置ID');
|
|
||||||
}
|
|
||||||
if ($w < self::WEIGHT_MIN || $w > self::WEIGHT_MAX) {
|
|
||||||
throw new ApiException('权重必须在 ' . self::WEIGHT_MIN . '-' . self::WEIGHT_MAX . ' 之间');
|
|
||||||
}
|
|
||||||
$ids[] = $id;
|
|
||||||
$weightById[$id] = $w;
|
|
||||||
}
|
|
||||||
$list = $this->model->whereIn('id', array_unique($ids))->field('id,tier,grid_number')->select()->toArray();
|
|
||||||
$idToTier = [];
|
|
||||||
foreach ($list as $r) {
|
|
||||||
$id = isset($r['id']) ? (int) $r['id'] : 0;
|
|
||||||
$idToTier[$id] = isset($r['tier']) ? (string) $r['tier'] : '';
|
|
||||||
}
|
|
||||||
foreach ($weightById as $id => $w) {
|
|
||||||
$tier = $idToTier[$id] ?? '';
|
|
||||||
if ($tier === '') {
|
|
||||||
throw new ApiException('配置ID ' . $id . ' 不存在或档位为空');
|
|
||||||
}
|
|
||||||
DiceRewardConfig::where('id', $id)->update(['weight' => $w]);
|
|
||||||
}
|
|
||||||
DiceRewardConfig::refreshCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 测试时档位权重均为 0 的异常标识 */
|
/** 测试时档位权重均为 0 的异常标识 */
|
||||||
private const EXCEPTION_WEIGHT_ALL_ZERO = 'REWARD_WEIGHT_ALL_ZERO';
|
private const EXCEPTION_WEIGHT_ALL_ZERO = 'REWARD_WEIGHT_ALL_ZERO';
|
||||||
|
|
||||||
@@ -208,7 +170,10 @@ class DiceRewardConfigLogic extends BaseLogic
|
|||||||
throw new ApiException('测试次数仅支持 100、500、1000、5000、10000');
|
throw new ApiException('测试次数仅支持 100、500、1000、5000、10000');
|
||||||
}
|
}
|
||||||
|
|
||||||
$grouped = $this->getListGroupedByTier();
|
$grouped = [];
|
||||||
|
foreach (['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'] as $t) {
|
||||||
|
$grouped[$t] = $this->model::getCachedByTierForDirection($t, 0);
|
||||||
|
}
|
||||||
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
|
||||||
$tierWeights = [1, 1, 1, 1, 1];
|
$tierWeights = [1, 1, 1, 1, 1];
|
||||||
$config = null;
|
$config = null;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
namespace app\dice\logic\reward_config_record;
|
namespace app\dice\logic\reward_config_record;
|
||||||
|
|
||||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
use app\dice\model\reward_config\DiceRewardConfig;
|
use app\dice\model\reward\DiceReward;
|
||||||
|
use app\dice\model\reward\DiceRewardConfig;
|
||||||
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
use app\dice\model\reward_config_record\DiceRewardConfigRecord;
|
||||||
use plugin\saiadmin\basic\think\BaseLogic;
|
use plugin\saiadmin\basic\think\BaseLogic;
|
||||||
use plugin\saiadmin\exception\ApiException;
|
use plugin\saiadmin\exception\ApiException;
|
||||||
@@ -70,7 +71,7 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将测试记录的权重导入到正式配置:weight_config_snapshot → DiceRewardConfig,tier_weights_snapshot → DiceLotteryPoolConfig,并刷新缓存
|
* 将测试记录的权重导入:weight_config_snapshot → dice_reward(顺时针/逆时针同值),tier_weights_snapshot → DiceLotteryPoolConfig,并刷新缓存
|
||||||
* @param int $recordId 测试记录 ID
|
* @param int $recordId 测试记录 ID
|
||||||
* @param int|null $lotteryConfigId 要导入档位权重的奖池配置 ID;不传则使用记录中的 lottery_config_id(若有)
|
* @param int|null $lotteryConfigId 要导入档位权重的奖池配置 ID;不传则使用记录中的 lottery_config_id(若有)
|
||||||
* @throws ApiException
|
* @throws ApiException
|
||||||
@@ -91,11 +92,29 @@ class DiceRewardConfigRecordLogic extends BaseLogic
|
|||||||
foreach ($snapshot as $item) {
|
foreach ($snapshot as $item) {
|
||||||
$id = isset($item['id']) ? (int) $item['id'] : 0;
|
$id = isset($item['id']) ? (int) $item['id'] : 0;
|
||||||
$weight = isset($item['weight']) ? (int) $item['weight'] : 1;
|
$weight = isset($item['weight']) ? (int) $item['weight'] : 1;
|
||||||
if ($id > 0) {
|
$weight = max(1, min(10000, $weight));
|
||||||
DiceRewardConfig::where('id', $id)->update(['weight' => $weight]);
|
if ($id <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$tier = DiceRewardConfig::where('id', $id)->value('tier');
|
||||||
|
if ($tier === null || $tier === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$tier = (string) $tier;
|
||||||
|
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) {
|
||||||
|
$m = new DiceReward();
|
||||||
|
$m->tier = $tier;
|
||||||
|
$m->direction = $direction;
|
||||||
|
$m->end_index = $id;
|
||||||
|
$m->weight = $weight;
|
||||||
|
$m->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DiceReward::refreshCache();
|
||||||
|
}
|
||||||
|
|
||||||
$tierSnapshot = $record['tier_weights_snapshot'] ?? null;
|
$tierSnapshot = $record['tier_weights_snapshot'] ?? null;
|
||||||
if (is_string($tierSnapshot)) {
|
if (is_string($tierSnapshot)) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace app\dice\model\play_record;
|
|||||||
|
|
||||||
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
|
||||||
use app\dice\model\player\DicePlayer;
|
use app\dice\model\player\DicePlayer;
|
||||||
use app\dice\model\reward_config\DiceRewardConfig;
|
use app\dice\model\reward\DiceRewardConfig;
|
||||||
use plugin\saiadmin\basic\think\BaseModel;
|
use plugin\saiadmin\basic\think\BaseModel;
|
||||||
use think\model\relation\BelongsTo;
|
use think\model\relation\BelongsTo;
|
||||||
|
|
||||||
|
|||||||
139
server/app/dice/model/reward/DiceReward.php
Normal file
139
server/app/dice/model/reward/DiceReward.php
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | saiadmin [ saiadmin快速开发框架 ]
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
namespace app\dice\model\reward;
|
||||||
|
|
||||||
|
use plugin\saiadmin\basic\think\BaseModel;
|
||||||
|
use support\think\Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 奖励对照模型
|
||||||
|
*
|
||||||
|
* dice_reward 奖励对照表(主键 id 自增)
|
||||||
|
* 唯一约束 (direction, grid_number),保证每个点数、每个方向各一条;end_index 关联 DiceRewardConfig.id
|
||||||
|
*
|
||||||
|
* @property $id 主键
|
||||||
|
* @property $tier 档位 T1-T5/BIGWIN
|
||||||
|
* @property $direction 方向:0=顺时针,1=逆时针
|
||||||
|
* @property $end_index 结束索引(DiceRewardConfig.id)
|
||||||
|
* @property $weight 权重 1-10000,档位内按权重比抽取
|
||||||
|
* @property $grid_number 色子点数(摇取值)
|
||||||
|
* @property $start_index 起始索引(DiceRewardConfig.id)
|
||||||
|
* @property $ui_text 显示文本(来自config)
|
||||||
|
* @property $real_ev 实际中奖金额(来自config)
|
||||||
|
* @property $remark 备注(来自config)
|
||||||
|
* @property $type 奖励类型(来自config)
|
||||||
|
*/
|
||||||
|
class DiceReward extends BaseModel
|
||||||
|
{
|
||||||
|
/** 方向:顺时针 */
|
||||||
|
public const DIRECTION_CLOCKWISE = 0;
|
||||||
|
/** 方向:逆时针 */
|
||||||
|
public const DIRECTION_COUNTERCLOCKWISE = 1;
|
||||||
|
|
||||||
|
/** 缓存键:奖励对照实例 */
|
||||||
|
private const CACHE_KEY_INSTANCE = 'dice:reward:instance';
|
||||||
|
|
||||||
|
private const CACHE_TTL = 86400 * 30;
|
||||||
|
|
||||||
|
private static ?array $instance = null;
|
||||||
|
|
||||||
|
protected $table = 'dice_reward';
|
||||||
|
|
||||||
|
/** 主键 id 自增,唯一约束 (direction, grid_number) */
|
||||||
|
protected $pk = 'id';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取奖励对照实例(按档位+方向索引,用于抽奖与权重配比)
|
||||||
|
* @return array{list: array, by_tier_direction: array}
|
||||||
|
*/
|
||||||
|
public static function getCachedInstance(): array
|
||||||
|
{
|
||||||
|
if (self::$instance !== null) {
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
$instance = Cache::get(self::CACHE_KEY_INSTANCE);
|
||||||
|
if ($instance !== null && is_array($instance)) {
|
||||||
|
self::$instance = $instance;
|
||||||
|
return $instance;
|
||||||
|
}
|
||||||
|
self::refreshCache();
|
||||||
|
$instance = Cache::get(self::CACHE_KEY_INSTANCE);
|
||||||
|
self::$instance = is_array($instance) ? $instance : self::buildEmptyInstance();
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按档位+方向取权重列表(用于抽奖:该档位该方向下 end_index => weight)
|
||||||
|
* @return array<int, int> end_index => weight
|
||||||
|
*/
|
||||||
|
public static function getCachedByTierAndDirection(string $tier, int $direction): array
|
||||||
|
{
|
||||||
|
$inst = self::getCachedInstance();
|
||||||
|
$byTierDirection = $inst['by_tier_direction'] ?? [];
|
||||||
|
$list = $byTierDirection[$tier][$direction] ?? [];
|
||||||
|
$result = [];
|
||||||
|
foreach ($list as $row) {
|
||||||
|
$endIndex = isset($row['end_index']) ? (int) $row['end_index'] : 0;
|
||||||
|
$weight = isset($row['weight']) ? (int) $row['weight'] : 1;
|
||||||
|
$result[$endIndex] = $weight;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新从数据库加载并写入缓存;修改/新增/删除后需调用以实例化
|
||||||
|
*/
|
||||||
|
public static function refreshCache(): void
|
||||||
|
{
|
||||||
|
$list = (new self())->order('tier')->order('direction')->order('end_index')->select()->toArray();
|
||||||
|
$byTierDirection = [];
|
||||||
|
foreach ($list as $row) {
|
||||||
|
$tier = isset($row['tier']) ? (string) $row['tier'] : '';
|
||||||
|
$direction = isset($row['direction']) ? (int) $row['direction'] : 0;
|
||||||
|
if ($tier !== '') {
|
||||||
|
if (!isset($byTierDirection[$tier])) {
|
||||||
|
$byTierDirection[$tier] = [0 => [], 1 => []];
|
||||||
|
}
|
||||||
|
if (!isset($byTierDirection[$tier][$direction])) {
|
||||||
|
$byTierDirection[$tier][$direction] = [];
|
||||||
|
}
|
||||||
|
$byTierDirection[$tier][$direction][] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self::$instance = [
|
||||||
|
'list' => $list,
|
||||||
|
'by_tier_direction' => $byTierDirection,
|
||||||
|
];
|
||||||
|
Cache::set(self::CACHE_KEY_INSTANCE, self::$instance, self::CACHE_TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function buildEmptyInstance(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'list' => [],
|
||||||
|
'by_tier_direction' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function clearRequestInstance(): void
|
||||||
|
{
|
||||||
|
self::$instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function onAfterInsert($model): void
|
||||||
|
{
|
||||||
|
self::refreshCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function onAfterUpdate($model): void
|
||||||
|
{
|
||||||
|
self::refreshCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function onAfterDelete($model): void
|
||||||
|
{
|
||||||
|
self::refreshCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
9
server/app/dice/model/reward/DiceRewardConfig.php
Normal file
9
server/app/dice/model/reward/DiceRewardConfig.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
// | 别名:reward 命名空间下引用 reward_config\DiceRewardConfig
|
||||||
|
// +----------------------------------------------------------------------
|
||||||
|
namespace app\dice\model\reward;
|
||||||
|
|
||||||
|
class DiceRewardConfig extends \app\dice\model\reward_config\DiceRewardConfig
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -6,24 +6,23 @@
|
|||||||
// +----------------------------------------------------------------------
|
// +----------------------------------------------------------------------
|
||||||
namespace app\dice\model\reward_config;
|
namespace app\dice\model\reward_config;
|
||||||
|
|
||||||
|
use app\dice\model\reward\DiceReward;
|
||||||
use plugin\saiadmin\basic\think\BaseModel;
|
use plugin\saiadmin\basic\think\BaseModel;
|
||||||
use support\think\Cache;
|
use support\think\Cache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 奖励配置模型
|
* 奖励配置模型
|
||||||
*
|
*
|
||||||
* dice_reward_config 奖励配置
|
* dice_reward_config 奖励配置;BIGWIN 档位使用本表 weight(0-10000,10000=100% 中大奖)
|
||||||
* 按档位 T1-T5 直接权重抽取 grid_number,weight 1-10000;起始索引 s_start_index / n_start_index
|
|
||||||
*
|
*
|
||||||
* @property $id ID
|
* @property $id ID
|
||||||
* @property $grid_number 色子点数
|
* @property $grid_number 色子点数
|
||||||
* @property $ui_text 前端显示文本
|
* @property $ui_text 前端显示文本
|
||||||
* @property $real_ev 真实资金结算
|
* @property $real_ev 真实资金结算
|
||||||
* @property $tier 所属档位
|
* @property $tier 所属档位
|
||||||
* @property $weight 权重 1-10000,档位内按权重比抽取
|
* @property $weight 权重(仅 BIGWIN 使用,0-10000)
|
||||||
* @property $n_start_index 逆时针起始索引
|
|
||||||
* @property $s_start_index 顺时针起始索引
|
|
||||||
* @property $remark 备注
|
* @property $remark 备注
|
||||||
|
* @property $type 奖励类型 -2=唯一惩罚,-1=抽水,0=回本,1=再来一次,2=小赚,3=大奖格
|
||||||
* @property $create_time 创建时间
|
* @property $create_time 创建时间
|
||||||
* @property $update_time 修改时间
|
* @property $update_time 修改时间
|
||||||
*/
|
*/
|
||||||
@@ -129,7 +128,7 @@ class DiceRewardConfig extends BaseModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从缓存按档位取奖励列表(含 weight,用于按权重抽 grid_number)
|
* 从缓存按档位取奖励列表(不含权重,仅配置)
|
||||||
*/
|
*/
|
||||||
public static function getCachedByTier(string $tier): array
|
public static function getCachedByTier(string $tier): array
|
||||||
{
|
{
|
||||||
@@ -138,6 +137,22 @@ class DiceRewardConfig extends BaseModel
|
|||||||
return $byTier[$tier] ?? [];
|
return $byTier[$tier] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按档位+方向取奖励列表(合并 dice_reward 权重,用于抽奖)
|
||||||
|
* @param int $direction 0=顺时针, 1=逆时针
|
||||||
|
* @return array 每行含 id, grid_number, real_ev, tier, weight 等
|
||||||
|
*/
|
||||||
|
public static function getCachedByTierForDirection(string $tier, int $direction): array
|
||||||
|
{
|
||||||
|
$list = self::getCachedByTier($tier);
|
||||||
|
$weightMap = DiceReward::getCachedByTierAndDirection($tier, $direction);
|
||||||
|
foreach ($list as $i => $row) {
|
||||||
|
$id = isset($row['id']) ? (int) $row['id'] : 0;
|
||||||
|
$list[$i]['weight'] = $weightMap[$id] ?? 1;
|
||||||
|
}
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
public static function clearRequestInstance(): void
|
public static function clearRequestInstance(): void
|
||||||
{
|
{
|
||||||
self::$instance = null;
|
self::$instance = null;
|
||||||
@@ -199,18 +214,4 @@ class DiceRewardConfig extends BaseModel
|
|||||||
$query->where('tier', '=', $value);
|
$query->where('tier', '=', $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function searchWeightMinAttr($query, $value)
|
|
||||||
{
|
|
||||||
if ($value !== '' && $value !== null) {
|
|
||||||
$query->where('weight', '>=', $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function searchWeightMaxAttr($query, $value)
|
|
||||||
{
|
|
||||||
if ($value !== '' && $value !== null) {
|
|
||||||
$query->where('weight', '<=', $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,22 +9,17 @@ namespace app\dice\validate\reward_config;
|
|||||||
use plugin\saiadmin\basic\BaseValidate;
|
use plugin\saiadmin\basic\BaseValidate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 奖励配置验证器(DiceRewardConfig)
|
* 奖励配置验证器(DiceRewardConfig;BIGWIN 的 weight 存本表,0-10000)
|
||||||
* weight 1-10000,各档位权重和不限制
|
|
||||||
*/
|
*/
|
||||||
class DiceRewardConfigValidate extends BaseValidate
|
class DiceRewardConfigValidate extends BaseValidate
|
||||||
{
|
{
|
||||||
private const WEIGHT_MIN = 1;
|
|
||||||
private const WEIGHT_MAX = 10000;
|
|
||||||
|
|
||||||
protected $rule = [
|
protected $rule = [
|
||||||
'grid_number' => 'require',
|
'grid_number' => 'require',
|
||||||
'ui_text' => 'require',
|
'ui_text' => 'require',
|
||||||
'real_ev' => 'require',
|
'real_ev' => 'require',
|
||||||
'tier' => 'require',
|
'tier' => 'require',
|
||||||
'weight' => 'checkWeight',
|
'type' => 'number',
|
||||||
'n_start_index' => 'number',
|
'weight' => 'number|between:0,10000', // BIGWIN 大奖权重,仅档位为 BIGWIN 时使用
|
||||||
's_start_index' => 'number',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $message = [
|
protected $message = [
|
||||||
@@ -32,28 +27,11 @@ class DiceRewardConfigValidate extends BaseValidate
|
|||||||
'ui_text' => '前端显示文本必须填写',
|
'ui_text' => '前端显示文本必须填写',
|
||||||
'real_ev' => '真实资金结算必须填写',
|
'real_ev' => '真实资金结算必须填写',
|
||||||
'tier' => '所属档位必须填写',
|
'tier' => '所属档位必须填写',
|
||||||
'weight' => '权重必须为 1-10000',
|
'type' => '奖励类型须为数字',
|
||||||
'n_start_index' => '逆时针起始索引须为数字',
|
|
||||||
's_start_index' => '顺时针起始索引须为数字',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $scene = [
|
protected $scene = [
|
||||||
'save' => ['grid_number', 'ui_text', 'real_ev', 'tier', 'weight', 'n_start_index', 's_start_index'],
|
'save' => ['grid_number', 'ui_text', 'real_ev', 'tier', 'type'],
|
||||||
'update' => ['grid_number', 'ui_text', 'real_ev', 'tier', 'weight', 'n_start_index', 's_start_index'],
|
'update' => ['grid_number', 'ui_text', 'real_ev', 'tier', 'type', 'weight'],
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
|
||||||
* weight:1-10000
|
|
||||||
*/
|
|
||||||
protected function checkWeight($value, $rule = '', $data = []): bool
|
|
||||||
{
|
|
||||||
$num = is_numeric($value) ? (int) $value : null;
|
|
||||||
if ($num === null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if ($num < self::WEIGHT_MIN || $num > self::WEIGHT_MAX) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,9 +102,15 @@ Route::group('/core', function () {
|
|||||||
Route::post('/dice/player_wallet_record/DicePlayerWalletRecord/adminOperate', [\app\dice\controller\player_wallet_record\DicePlayerWalletRecordController::class, 'adminOperate']);
|
Route::post('/dice/player_wallet_record/DicePlayerWalletRecord/adminOperate', [\app\dice\controller\player_wallet_record\DicePlayerWalletRecordController::class, 'adminOperate']);
|
||||||
fastRoute('dice/player_ticket_record/DicePlayerTicketRecord', \app\dice\controller\player_ticket_record\DicePlayerTicketRecordController::class);
|
fastRoute('dice/player_ticket_record/DicePlayerTicketRecord', \app\dice\controller\player_ticket_record\DicePlayerTicketRecordController::class);
|
||||||
Route::get('/dice/player_ticket_record/DicePlayerTicketRecord/getPlayerOptions', [\app\dice\controller\player_ticket_record\DicePlayerTicketRecordController::class, 'getPlayerOptions']);
|
Route::get('/dice/player_ticket_record/DicePlayerTicketRecord/getPlayerOptions', [\app\dice\controller\player_ticket_record\DicePlayerTicketRecordController::class, 'getPlayerOptions']);
|
||||||
|
fastRoute('dice/reward/DiceReward', \app\dice\controller\reward\DiceRewardController::class);
|
||||||
|
Route::get('/dice/reward/DiceReward/weightRatioList', [\app\dice\controller\reward\DiceRewardController::class, 'weightRatioList']);
|
||||||
|
Route::get('/dice/reward/DiceReward/weightRatioListWithDirection', [\app\dice\controller\reward\DiceRewardController::class, 'weightRatioListWithDirection']);
|
||||||
|
Route::post('/dice/reward/DiceReward/batchUpdateWeights', [\app\dice\controller\reward\DiceRewardController::class, 'batchUpdateWeights']);
|
||||||
|
Route::post('/dice/reward/DiceReward/batchUpdateWeightsByDirection', [\app\dice\controller\reward\DiceRewardController::class, 'batchUpdateWeightsByDirection']);
|
||||||
fastRoute('dice/reward_config/DiceRewardConfig', \app\dice\controller\reward_config\DiceRewardConfigController::class);
|
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::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/batchUpdateWeights', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'batchUpdateWeights']);
|
||||||
|
Route::post('/dice/reward_config/DiceRewardConfig/createRewardReference', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'createRewardReference']);
|
||||||
Route::post('/dice/reward_config/DiceRewardConfig/runWeightTest', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'runWeightTest']);
|
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);
|
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/getOptions', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'getOptions']);
|
||||||
|
|||||||
Reference in New Issue
Block a user