Compare commits

..

6 Commits

32 changed files with 1295 additions and 343 deletions

View File

@@ -1,79 +0,0 @@
import request from '@/utils/http'
/**
* 色子奖池配置 API接口
*/
export default {
/**
* 获取数据列表DiceLotteryConfig
* @param params 搜索参数
* @returns 数据列表
*/
list(params: Record<string, any>) {
return request.get<Api.Common.ApiPage>({
url: '/dice/lottery_config/DiceLotteryConfig/index',
params
})
},
/**
* 获取 DiceLotteryConfig 列表数据,仅含 id、name用于 lottery_config_id 下拉
* @returns DiceLotteryConfig['id','name'] 列表
*/
async getOptions(): Promise<Array<{ id: number; name: string }>> {
const res = await request.get<any>({
url: '/dice/lottery_config/DiceLotteryConfig/getOptions'
})
const rows = (res?.data ?? []) as Array<{ id: number; name: string }>
return Array.isArray(rows)
? rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') }))
: []
},
/**
* 读取数据
* @param id 数据ID
* @returns 数据详情
*/
read(id: number | string) {
return request.get<Api.Common.ApiData>({
url: '/dice/lottery_config/DiceLotteryConfig/read?id=' + id
})
},
/**
* 创建数据
* @param params 数据参数
* @returns 执行结果
*/
save(params: Record<string, any>) {
return request.post<any>({
url: '/dice/lottery_config/DiceLotteryConfig/save',
data: params
})
},
/**
* 更新数据
* @param params 数据参数
* @returns 执行结果
*/
update(params: Record<string, any>) {
return request.put<any>({
url: '/dice/lottery_config/DiceLotteryConfig/update',
data: params
})
},
/**
* 删除数据
* @param id 数据ID
* @returns 执行结果
*/
delete(params: Record<string, any>) {
return request.del<any>({
url: '/dice/lottery_config/DiceLotteryConfig/destroy',
data: params
})
}
}

View File

@@ -0,0 +1,115 @@
import request from '@/utils/http'
/**
* 色子奖池配置 API 接口
*/
export default {
/**
* 获取数据列表DiceLotteryPoolConfig
* @param params 搜索参数
* @returns 数据列表
*/
list(params: Record<string, any>) {
return request.get<Api.Common.ApiPage>({
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/index',
params
})
},
/**
* 获取 DiceLotteryPoolConfig 列表数据,仅含 id、name用于 lottery_config_id 下拉
* @returns DiceLotteryPoolConfig['id','name'] 列表
*/
async getOptions(): Promise<Array<{ id: number; name: string }>> {
const res = await request.get<any>({
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions'
})
const rows = (res?.data ?? []) as Array<{ id: number; name: string }>
return Array.isArray(rows)
? rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') }))
: []
},
/**
* 读取数据
* @param id 数据ID
* @returns 数据详情
*/
read(id: number | string) {
return request.get<Api.Common.ApiData>({
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/read?id=' + id
})
},
/**
* 创建数据
* @param params 数据参数
* @returns 执行结果
*/
save(params: Record<string, any>) {
return request.post<any>({
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/save',
data: params
})
},
/**
* 更新数据
* @param params 数据参数
* @returns 执行结果
*/
update(params: Record<string, any>) {
return request.put<any>({
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/update',
data: params
})
},
/**
* 删除数据
* @param id 数据ID
* @returns 执行结果
*/
delete(params: Record<string, any>) {
return request.del<any>({
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/destroy',
data: params
})
},
/**
* 获取当前彩金池Redis 实例化,无则按 type=0 创建),含 profit_amount 实时值
*/
getCurrentPool() {
return request.get<{
id: number
name: string
safety_line: number
t1_weight: number
t2_weight: number
t3_weight: number
t4_weight: number
t5_weight: number
profit_amount: number
}>({
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool'
})
},
/**
* 更新当前彩金池:仅 safety_line、t1_weightt5_weight不可改 profit_amount
*/
updateCurrentPool(params: {
safety_line?: number
t1_weight?: number
t2_weight?: number
t3_weight?: number
t4_weight?: number
t5_weight?: number
}) {
return request.post<any>({
url: '/dice/lottery_pool_config/DiceLotteryPoolConfig/updateCurrentPool',
data: params
})
}
}

View File

@@ -74,7 +74,7 @@ export default {
}, },
/** /**
* 获取彩金池配置选项DiceLotteryConfig.id、name供 lottery_config_id 下拉使用 * 获取彩金池配置选项DiceLotteryPoolConfig.id、name供 lottery_config_id 下拉使用
* @returns [ { id, name } ] * @returns [ { id, name } ]
*/ */
async getLotteryConfigOptions(): Promise<Array<{ id: number; name: string }>> { async getLotteryConfigOptions(): Promise<Array<{ id: number; name: string }>> {

View File

@@ -61,5 +61,24 @@ export default {
url: '/core/dice/reward_config/DiceRewardConfig/destroy', url: '/core/dice/reward_config/DiceRewardConfig/destroy',
data: params data: params
}) })
},
/**
* T1-T5、BIGWIN 权重配比:按档位分组获取配置列表
*/
weightRatioList() {
return request.get<Api.Common.ApiData>({
url: '/core/dice/reward_config/DiceRewardConfig/weightRatioList'
})
},
/**
* T1-T5、BIGWIN 权重配比:批量更新权重(同一档位权重之和必须等于 100%
*/
batchUpdateWeights(items: Array<{ id: number; weight: number }>) {
return request.post<any>({
url: '/core/dice/reward_config/DiceRewardConfig/batchUpdateWeights',
data: { items }
})
} }
} }

View File

@@ -7,29 +7,13 @@
<!-- 表格头部 --> <!-- 表格头部 -->
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData"> <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left> <template #left>
<!-- <ElSpace wrap>--> <ElButton
<!-- <ElButton--> v-permission="'dice:lottery_pool_config:index:index'"
<!-- v-permission="'dice:lottery_config:index:save'"--> type="primary"
<!-- @click="showDialog('add')"--> @click="showCurrentPoolDialog"
<!-- v-ripple--> >
<!-- >--> 查看当前彩金池
<!-- <template #icon>--> </ElButton>
<!-- <ArtSvgIcon icon="ri:add-fill" />-->
<!-- </template>-->
<!-- 新增-->
<!-- </ElButton>-->
<!-- <ElButton-->
<!-- v-permission="'dice:lottery_config:index:destroy'"-->
<!-- :disabled="selectedRows.length === 0"-->
<!-- @click="deleteSelectedRows(api.delete, refreshData)"-->
<!-- v-ripple-->
<!-- >-->
<!-- <template #icon>-->
<!-- <ArtSvgIcon icon="ri:delete-bin-5-line" />-->
<!-- </template>-->
<!-- 删除-->
<!-- </ElButton>-->
<!-- </ElSpace>-->
</template> </template>
</ArtTableHeader> </ArtTableHeader>
@@ -50,12 +34,12 @@
<template #operation="{ row }"> <template #operation="{ row }">
<div class="flex gap-2"> <div class="flex gap-2">
<SaButton <SaButton
v-permission="'dice:lottery_config:index:update'" v-permission="'dice:lottery_pool_config:index:update'"
type="secondary" type="secondary"
@click="showDialog('edit', row)" @click="showDialog('edit', row)"
/> />
<!-- <SaButton--> <!-- <SaButton-->
<!-- v-permission="'dice:lottery_config:index:destroy'"--> <!-- v-permission="'dice:lottery_pool_config:index:destroy'"-->
<!-- type="error"--> <!-- type="error"-->
<!-- @click="deleteRow(row, api.delete, refreshData)"--> <!-- @click="deleteRow(row, api.delete, refreshData)"-->
<!-- />--> <!-- />-->
@@ -71,15 +55,18 @@
:data="dialogData" :data="dialogData"
@success="refreshData" @success="refreshData"
/> />
<!-- 当前彩金池弹窗 -->
<CurrentPoolDialog v-model="currentPoolVisible" @success="refreshData" />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
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/lottery_config/index' import api from '../../api/lottery_pool_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 CurrentPoolDialog from './modules/current-pool-dialog.vue'
// //
const searchForm = ref({ const searchForm = ref({
@@ -172,14 +159,10 @@
}) })
// //
const { const { dialogType, dialogVisible, dialogData, showDialog, handleSelectionChange } = useSaiAdmin()
dialogType,
dialogVisible, const currentPoolVisible = ref(false)
dialogData, function showCurrentPoolDialog() {
showDialog, currentPoolVisible.value = true
// deleteRow, }
// deleteSelectedRows,
handleSelectionChange
// selectedRows
} = useSaiAdmin()
</script> </script>

View File

@@ -0,0 +1,292 @@
<template>
<el-dialog
v-model="visible"
title="当前彩金池"
width="560px"
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<div v-if="loading && !pool" class="flex justify-center py-8">加载中...</div>
<template v-else-if="pool">
<div class="pool-info mb-4">
<div class="flex items-center gap-2 mb-3">
<span class="text-gray-500">池子名称</span>
<span>{{ pool.name }}</span>
</div>
<div class="profit-row mb-3">
<div class="flex items-center gap-2">
<span class="text-gray-500">彩金池盈利profit_amount</span>
<span class="font-mono text-lg" :class="profitAmountClass">{{ displayProfitAmount }}</span>
<span class="realtime-badge">实时</span>
</div>
<div class="profit-calc-hint">
计算方式每局抽奖累加 (100 该局中奖档位 real_ev)弹窗打开期间每 2 秒自动刷新
</div>
</div>
<div class="tip-block">
<div class="tip-title">抽奖档位规则</div>
<div class="tip-content">
当彩金池盈利 <strong>低于安全线</strong> <strong>玩家</strong> T*_weight 权重抽取抽奖档位
当彩金池盈利 <strong>高于或等于安全线</strong> <strong>当前彩金池</strong> T*_weight 权重抽取档位
</div>
</div>
</div>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item label="安全线" prop="safety_line">
<el-input-number
v-model="formData.safety_line"
:min="0"
:precision="2"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="T1池权重(%)" prop="t1_weight">
<el-slider v-model="formData.t1_weight" :min="0" :max="100" :step="1" show-input />
</el-form-item>
<el-form-item label="T2池权重(%)" prop="t2_weight">
<el-slider v-model="formData.t2_weight" :min="0" :max="100" :step="1" show-input />
</el-form-item>
<el-form-item label="T3池权重(%)" prop="t3_weight">
<el-slider v-model="formData.t3_weight" :min="0" :max="100" :step="1" show-input />
</el-form-item>
<el-form-item label="T4池权重(%)" prop="t4_weight">
<el-slider v-model="formData.t4_weight" :min="0" :max="100" :step="1" show-input />
</el-form-item>
<el-form-item label="T5池权重(%)" prop="t5_weight">
<el-slider v-model="formData.t5_weight" :min="0" :max="100" :step="1" show-input />
</el-form-item>
<el-form-item>
<div class="text-gray-500 text-sm">
五个池权重总和<span :class="weightsSum !== 100 ? 'text-red-500' : ''">{{
weightsSum
}}</span
>% / 100%100%
</div>
</el-form-item>
</el-form>
</template>
<template #footer>
<el-button @click="handleClose">关闭</el-button>
<el-button type="primary" :loading="saving" :disabled="!pool" @click="handleSubmit">
保存权重与安全线
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import api from '../../../api/lottery_pool_config/index'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
interface PoolData {
id: number
name: string
safety_line: number
t1_weight: number
t2_weight: number
t3_weight: number
t4_weight: number
t5_weight: number
profit_amount: number
}
const props = defineProps<{ modelValue: boolean }>()
const emit = defineEmits<{ (e: 'update:modelValue', v: boolean): void; (e: 'success'): void }>()
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const loading = ref(false)
const saving = ref(false)
const pool = ref<PoolData | null>(null)
const formRef = ref<FormInstance>()
const formData = reactive({
safety_line: 0,
t1_weight: 0,
t2_weight: 0,
t3_weight: 0,
t4_weight: 0,
t5_weight: 0
})
const rules: FormRules = {
safety_line: [{ required: true, message: '请输入安全线', trigger: 'blur' }],
t1_weight: [{ required: true, message: '请输入T1权重', trigger: 'blur' }],
t2_weight: [{ required: true, message: '请输入T2权重', trigger: 'blur' }],
t3_weight: [{ required: true, message: '请输入T3权重', trigger: 'blur' }],
t4_weight: [{ required: true, message: '请输入T4权重', trigger: 'blur' }],
t5_weight: [{ required: true, message: '请输入T5权重', trigger: 'blur' }]
}
const weightsSum = computed(
() =>
formData.t1_weight +
formData.t2_weight +
formData.t3_weight +
formData.t4_weight +
formData.t5_weight
)
const displayProfitAmount = computed(() => {
const v = pool.value?.profit_amount
if (v == null || Number.isNaN(v)) return '-'
return Number(v).toFixed(2)
})
const profitAmountClass = computed(() => {
const v = pool.value?.profit_amount
if (v == null) return ''
if (v > 0) return 'text-green-600'
if (v < 0) return 'text-red-600'
return ''
})
let pollTimer: ReturnType<typeof setInterval> | null = null
const POLL_INTERVAL = 2000
async function loadPool() {
if (!visible.value) return
try {
loading.value = true
const res = await api.getCurrentPool()
const data = res as unknown as PoolData
if (data && typeof data === 'object') {
pool.value = data
formData.safety_line = data.safety_line ?? 0
formData.t1_weight = data.t1_weight ?? 0
formData.t2_weight = data.t2_weight ?? 0
formData.t3_weight = data.t3_weight ?? 0
formData.t4_weight = data.t4_weight ?? 0
formData.t5_weight = data.t5_weight ?? 0
}
} catch (e: any) {
ElMessage.error(e?.message ?? '获取彩金池失败')
} finally {
loading.value = false
}
}
function startPolling() {
stopPolling()
pollTimer = setInterval(() => {
if (!visible.value) {
stopPolling()
return
}
api.getCurrentPool().then((res) => {
const data = res as unknown as PoolData
if (pool.value && data && typeof data === 'object' && data.profit_amount != null) {
pool.value.profit_amount = data.profit_amount
}
})
}, POLL_INTERVAL)
}
function stopPolling() {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
async function handleSubmit() {
if (!formRef.value || !pool.value) return
if (weightsSum.value !== 100) {
ElMessage.warning('T1T5 权重合计须为 100%')
return
}
try {
await formRef.value.validate()
saving.value = true
await api.updateCurrentPool({
safety_line: formData.safety_line,
t1_weight: formData.t1_weight,
t2_weight: formData.t2_weight,
t3_weight: formData.t3_weight,
t4_weight: formData.t4_weight,
t5_weight: formData.t5_weight
})
ElMessage.success('保存成功')
await loadPool()
emit('success')
} catch (e: any) {
if (e?.message) ElMessage.error(e.message)
} finally {
saving.value = false
}
}
function handleClose() {
stopPolling()
visible.value = false
pool.value = null
}
watch(
() => props.modelValue,
(open) => {
if (open) {
loadPool().then(() => startPolling())
} else {
stopPolling()
}
}
)
onUnmounted(() => stopPolling())
</script>
<style scoped>
.pool-info {
padding: 8px 0;
}
.profit-row {
margin-bottom: 8px;
}
.profit-calc-hint {
margin-top: 4px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 1.4;
}
.realtime-badge {
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
background: var(--el-color-success-light-9);
color: var(--el-color-success);
}
.tip-block {
margin-top: 12px;
padding: 10px 12px;
background: var(--el-fill-color-light);
border-radius: 6px;
border-left: 3px solid var(--el-color-primary);
}
.tip-title {
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
margin-bottom: 6px;
}
.tip-content {
font-size: 12px;
color: var(--el-text-color-regular);
line-height: 1.5;
}
.tip-content strong {
color: var(--el-color-primary);
}
</style>

View File

@@ -78,7 +78,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import api from '../../../api/lottery_config/index' import api from '../../../api/lottery_pool_config/index'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'

View File

@@ -133,7 +133,7 @@
const usernameFormatter = (row: Record<string, any>) => const usernameFormatter = (row: Record<string, any>) =>
row?.dicePlayer?.username ?? row?.player_id ?? '-' row?.dicePlayer?.username ?? row?.player_id ?? '-'
const lotteryConfigNameFormatter = (row: Record<string, any>) => const lotteryConfigNameFormatter = (row: Record<string, any>) =>
row?.diceLotteryConfig?.name ?? row?.lottery_config_id ?? '-' row?.diceLotteryPoolConfig?.name ?? row?.lottery_config_id ?? '-'
const rewardTierFormatter = (row: Record<string, any>) => const rewardTierFormatter = (row: Record<string, any>) =>
row?.diceRewardConfig?.tier ?? row?.reward_config_id ?? '-' row?.diceRewardConfig?.tier ?? row?.reward_config_id ?? '-'

View File

@@ -1,10 +1,10 @@
<template> <template>
<div class="art-full-height"> <div class="art-full-height">
<!-- 搜索面板 --> <!-- ???? -->
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" /> <TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
<ElCard class="art-table-card" shadow="never"> <ElCard class="art-table-card" shadow="never">
<!-- 表格头部 --> <!-- ???? -->
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData"> <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left> <template #left>
<ElSpace wrap> <ElSpace wrap>
@@ -12,7 +12,7 @@
<template #icon> <template #icon>
<ArtSvgIcon icon="ri:add-fill" /> <ArtSvgIcon icon="ri:add-fill" />
</template> </template>
新增 ??
</ElButton> </ElButton>
<ElButton <ElButton
v-permission="'dice:player:index:destroy'" v-permission="'dice:player:index:destroy'"
@@ -23,13 +23,13 @@
<template #icon> <template #icon>
<ArtSvgIcon icon="ri:delete-bin-5-line" /> <ArtSvgIcon icon="ri:delete-bin-5-line" />
</template> </template>
删除 ??
</ElButton> </ElButton>
</ElSpace> </ElSpace>
</template> </template>
</ArtTableHeader> </ArtTableHeader>
<!-- 表格 --> <!-- ?? -->
<ArtTable <ArtTable
ref="tableRef" ref="tableRef"
rowKey="id" rowKey="id"
@@ -42,7 +42,7 @@
@pagination:size-change="handleSizeChange" @pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange" @pagination:current-change="handleCurrentChange"
> >
<!-- 状态开关直接修改 --> <!-- ????????? -->
<template #status="{ row }"> <template #status="{ row }">
<ElSwitch <ElSwitch
v-permission="'dice:player:index:update'" v-permission="'dice:player:index:update'"
@@ -51,7 +51,7 @@
@change="(v: string | number | boolean) => handleStatusChange(row, v ? 1 : 0)" @change="(v: string | number | boolean) => handleStatusChange(row, v ? 1 : 0)"
/> />
</template> </template>
<!-- 平台币tag 可点击打开钱包操作弹窗 --> <!-- ????tag ??????????? -->
<template #coin="{ row }"> <template #coin="{ row }">
<ElTag <ElTag
type="info" type="info"
@@ -62,7 +62,7 @@
{{ row.coin ?? 0 }} {{ row.coin ?? 0 }}
</ElTag> </ElTag>
</template> </template>
<!-- 操作列 --> <!-- ??? -->
<template #operation="{ row }"> <template #operation="{ row }">
<div class="flex gap-2"> <div class="flex gap-2">
<SaButton <SaButton
@@ -80,7 +80,7 @@
</ArtTable> </ArtTable>
</ElCard> </ElCard>
<!-- 编辑弹窗 --> <!-- ???? -->
<EditDialog <EditDialog
v-model="dialogVisible" v-model="dialogVisible"
:dialog-type="dialogType" :dialog-type="dialogType"
@@ -88,7 +88,7 @@
@success="refreshData" @success="refreshData"
/> />
<!-- 钱包操作弹窗加点/扣点 --> <!-- ?????????/??? -->
<WalletOperateDialog <WalletOperateDialog
v-model="walletDialogVisible" v-model="walletDialogVisible"
:player="walletOperatePlayer" :player="walletOperatePlayer"
@@ -105,7 +105,7 @@
import EditDialog from './modules/edit-dialog.vue' import EditDialog from './modules/edit-dialog.vue'
import WalletOperateDialog from './modules/WalletOperateDialog.vue' import WalletOperateDialog from './modules/WalletOperateDialog.vue'
// 搜索表单 // ????
const searchForm = ref({ const searchForm = ref({
username: undefined, username: undefined,
name: undefined, name: undefined,
@@ -115,23 +115,23 @@
lottery_config_id: undefined lottery_config_id: undefined
}) })
// 搜索处理 // ????
const handleSearch = (params: Record<string, any>) => { const handleSearch = (params: Record<string, any>) => {
Object.assign(searchParams, params) Object.assign(searchParams, params)
getData() getData()
} }
// 权重列带 % formatterColumnOption.formatter 仅接收 row // ???? % ? formatter?ColumnOption.formatter ??? row?
const weightFormatter = (prop: string) => (row: any) => { const weightFormatter = (prop: string) => (row: any) => {
const cellValue = row[prop] const cellValue = row[prop]
return cellValue != null && cellValue !== '' ? `${cellValue}%` : '-' return cellValue != null && cellValue !== '' ? `${cellValue}%` : '-'
} }
// 彩金池配置列:lottery_config_id 关联 DiceLotteryConfig,显示 name // ???????lottery_config_id ?? DiceLotteryPoolConfig??? name
const lotteryConfigNameFormatter = (row: any) => const lotteryConfigNameFormatter = (row: any) =>
row?.diceLotteryConfig?.name ?? (row?.lottery_config_id ? `#${row.lottery_config_id}` : '自定义') row?.diceLotteryPoolConfig?.name ?? (row?.lottery_config_id ? `#${row.lottery_config_id}` : '???')
// 表格配置 // ????
const { const {
columns, columns,
columnChecks, columnChecks,
@@ -150,73 +150,73 @@
apiFn: api.list, apiFn: api.list,
columnsFactory: () => [ columnsFactory: () => [
{ type: 'selection' }, { type: 'selection' },
{ prop: 'username', label: '用户名', align: 'center' }, { prop: 'username', label: '???', align: 'center' },
{ prop: 'phone', label: '手机号', align: 'center' }, { prop: 'phone', label: '???', align: 'center' },
{ prop: 'name', label: '昵称', align: 'center' }, { prop: 'name', label: '??', align: 'center' },
{ {
prop: 'status', prop: 'status',
label: '状态', label: '??',
width: 88, width: 88,
align: 'center', align: 'center',
useSlot: true useSlot: true
}, },
{ {
prop: 'coin', prop: 'coin',
label: '平台币', label: '???',
width: 100, width: 100,
align: 'center', align: 'center',
useSlot: true useSlot: true
}, },
{ {
prop: 'lottery_config_id', prop: 'lottery_config_id',
label: '彩金池配置', label: '?????',
width: 120, width: 120,
align: 'center', align: 'center',
formatter: (row: any) => lotteryConfigNameFormatter(row) formatter: (row: any) => lotteryConfigNameFormatter(row)
}, },
{ {
prop: 't1_weight', prop: 't1_weight',
label: 'T1池权重', label: 'T1???',
width: 80, width: 80,
align: 'center', align: 'center',
formatter: weightFormatter('t1_weight') formatter: weightFormatter('t1_weight')
}, },
{ {
prop: 't2_weight', prop: 't2_weight',
label: 'T2池权重', label: 'T2???',
width: 100, width: 100,
align: 'center', align: 'center',
formatter: weightFormatter('t2_weight') formatter: weightFormatter('t2_weight')
}, },
{ {
prop: 't3_weight', prop: 't3_weight',
label: 'T3池权重', label: 'T3???',
width: 100, width: 100,
align: 'center', align: 'center',
formatter: weightFormatter('t3_weight') formatter: weightFormatter('t3_weight')
}, },
{ {
prop: 't4_weight', prop: 't4_weight',
label: 'T4池权重', label: 'T4???',
width: 100, width: 100,
align: 'center', align: 'center',
formatter: weightFormatter('t4_weight') formatter: weightFormatter('t4_weight')
}, },
{ {
prop: 't5_weight', prop: 't5_weight',
label: 'T5池权重', label: 'T5???',
width: 100, width: 100,
align: 'center', align: 'center',
formatter: weightFormatter('t5_weight') formatter: weightFormatter('t5_weight')
}, },
{ prop: 'total_ticket_count', label: '总抽奖次数', align: 'center' }, { prop: 'total_ticket_count', label: '?????', align: 'center' },
{ prop: 'paid_ticket_count', label: '购买抽奖次数', align: 'center' }, { prop: 'paid_ticket_count', label: '??????', align: 'center' },
{ prop: 'free_ticket_count', label: '赠送抽奖次数', align: 'center' }, { prop: 'free_ticket_count', label: '??????', align: 'center' },
{ prop: 'create_time', label: '创建时间', align: 'center' }, { prop: 'create_time', label: '????', align: 'center' },
{ prop: 'update_time', label: '更新时间', align: 'center' }, { prop: 'update_time', label: '????', align: 'center' },
{ {
prop: 'operation', prop: 'operation',
label: '操作', label: '??',
width: 100, width: 100,
align: 'center', align: 'center',
fixed: 'right', fixed: 'right',
@@ -226,7 +226,7 @@
} }
}) })
// 状态开关切换(列表内直接修改) // ???????????????
const handleStatusChange = async (row: Record<string, any>, status: number) => { const handleStatusChange = async (row: Record<string, any>, status: number) => {
row._statusLoading = true row._statusLoading = true
try { try {
@@ -239,7 +239,7 @@
} }
} }
// 编辑配置 // ????
const { const {
dialogType, dialogType,
dialogVisible, dialogVisible,
@@ -251,7 +251,7 @@
selectedRows selectedRows
} = useSaiAdmin() } = useSaiAdmin()
// 钱包操作弹窗(从平台币 tag 点击打开) // ??????????? tag ?????
const walletDialogVisible = ref(false) const walletDialogVisible = ref(false)
type WalletPlayer = { id: number; username?: string; coin?: number } type WalletPlayer = { id: number; username?: string; coin?: number }
const walletOperatePlayer = ref<WalletPlayer | null>(null) const walletOperatePlayer = ref<WalletPlayer | null>(null)

View File

@@ -170,7 +170,7 @@
<script setup lang="ts"> <script setup lang="ts">
import api from '../../../api/player/index' import api from '../../../api/player/index'
import lotteryConfigApi from '../../../api/lottery_config/index' import lotteryConfigApi from '../../../api/lottery_pool_config/index'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'

View File

@@ -79,7 +79,7 @@
const isExpanded = ref<boolean>(false) const isExpanded = ref<boolean>(false)
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([]) const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
/** 从玩家控制器获取 DiceLotteryConfig id/name 列表,用于 lottery_config_id 筛选 */ /** 从玩家控制器获取 DiceLotteryPoolConfig id/name 列表,用于 lottery_config_id 筛选 */
onMounted(async () => { onMounted(async () => {
try { try {
lotteryConfigOptions.value = await api.getLotteryConfigOptions() lotteryConfigOptions.value = await api.getLotteryConfigOptions()

View File

@@ -8,27 +8,14 @@
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData"> <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
<template #left> <template #left>
<ElSpace wrap> <ElSpace wrap>
<!-- <ElButton--> <ElButton
<!-- v-permission="'dice:reward_config:index:save'"--> v-permission="'dice:reward_config:index:update'"
<!-- @click="showDialog('add')"--> type="primary"
<!-- v-ripple--> @click="weightRatioVisible = true"
<!-- >--> v-ripple
<!-- <template #icon>--> >
<!-- <ArtSvgIcon icon="ri:add-fill" />--> T1-T5 BIGWIN 权重配比
<!-- </template>--> </ElButton>
<!-- 新增-->
<!-- </ElButton>-->
<!-- <ElButton-->
<!-- v-permission="'dice:reward_config:index:destroy'"-->
<!-- :disabled="selectedRows.length === 0"-->
<!-- @click="deleteSelectedRows(api.delete, refreshData)"-->
<!-- v-ripple-->
<!-- >-->
<!-- <template #icon>-->
<!-- <ArtSvgIcon icon="ri:delete-bin-5-line" />-->
<!-- </template>-->
<!-- 删除-->
<!-- </ElButton>-->
</ElSpace> </ElSpace>
</template> </template>
</ArtTableHeader> </ArtTableHeader>
@@ -71,6 +58,8 @@
:data="dialogData" :data="dialogData"
@success="refreshData" @success="refreshData"
/> />
<!-- T1-T5BIGWIN 权重配比弹窗 -->
<WeightRatioDialog v-model="weightRatioVisible" @success="refreshData" />
</div> </div>
</template> </template>
@@ -80,6 +69,9 @@
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'
const weightRatioVisible = ref(false)
// 搜索表单 // 搜索表单
const searchForm = ref<Record<string, unknown>>({ const searchForm = ref<Record<string, unknown>>({

View File

@@ -102,9 +102,11 @@
set: (value) => emit('update:modelValue', value) set: (value) => emit('update:modelValue', value)
}) })
/** tier=BIGWIN 且 grid_number 为 5 或 30 时权重固定 100%不可修改 */ /** BIGWIN 且 grid_number 为 5 或 30 时豹子概率不可修改 */
const isWeightFixed100 = computed( const isWeightDisabled = computed(
() => formData.tier === 'BIGWIN' && (formData.grid_number === 5 || formData.grid_number === 30) () =>
formData.tier === 'BIGWIN' &&
(formData.grid_number === 5 || formData.grid_number === 30)
) )
/** /**
@@ -230,10 +232,14 @@
payload.weight = 0 payload.weight = 0
} else if (payload.grid_number === 5 || payload.grid_number === 30) { } else if (payload.grid_number === 5 || payload.grid_number === 30) {
payload.weight = 100 payload.weight = 100
} else {
if (payload.grid_number === 5 || payload.grid_number === 30) {
payload.weight = 100
} else { } else {
const w = Number(payload.weight) const w = Number(payload.weight)
payload.weight = Number.isNaN(w) ? 0 : Math.max(0, Math.min(100, w)) payload.weight = Number.isNaN(w) ? 0 : Math.max(0, Math.min(100, w))
} }
}
if (props.dialogType === 'add') { if (props.dialogType === 'add') {
await api.save(payload) await api.save(payload)
ElMessage.success('新增成功') ElMessage.success('新增成功')

View File

@@ -0,0 +1,375 @@
<template>
<el-dialog
v-model="visible"
title="T1-T5 与 BIGWIN 权重配比"
width="600px"
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<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">
<ArtBarChart
:x-axis-data="getTierChartLabels(t)"
:data="getTierChartData(t)"
height="220px"
/>
</div>
<div class="weight-sum" v-if="t !== 'BIGWIN'">
当前档位权重合计<strong :class="{ over: getTierSumForValidation(t) !== 100 }">{{
getTierSumForValidation(t).toFixed(1)
}}</strong>
/ 100须等于 100%
</div>
<div class="weight-sum weight-sum-bigwin" v-else>
BIGWIN 为豹子权重单独设定每条 0100%无合计要求
</div>
<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="实际中奖金额"
prop="real_ev"
width="90"
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="权重(%)" min-width="200" align="center">
<template #default="{ row }">
<div class="weight-cell-vertical">
<div class="weight-slider-wrap">
<el-slider
:model-value="getItemWeight(row)"
:min="0"
:max="100"
:step="1"
size="small"
:disabled="isWeightDisabled(row, t)"
class="weight-slider"
@update:model-value="
(v: number | number[]) => setItemWeight(row, normalizeSliderValue(v))
"
/>
</div>
<div class="weight-input-wrap">
<el-button
type="primary"
link
:disabled="isWeightDisabled(row, t) || getItemWeight(row) <= 0"
@click="adjustWeight(row, -1)"
>
</el-button>
<el-input-number
:model-value="getItemWeight(row)"
:min="0"
:max="100"
:step="1"
:precision="1"
:disabled="isWeightDisabled(row, t)"
controls-position="right"
size="small"
class="weight-input"
@update:model-value="(v: number | undefined) => setItemWeight(row, v ?? 0)"
/>
<el-button
type="primary"
link
:disabled="isWeightDisabled(row, t) || getItemWeight(row) >= 100"
@click="adjustWeight(row, 1)"
>
</el-button>
</div>
</div>
</template>
</el-table-column>
</el-table>
</template>
</el-tab-pane>
</el-tabs>
<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_config/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', 'BIGWIN'] as const
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 tierKeys = TIER_KEYS
const activeTier = ref('T1')
const submitting = ref(false)
/** 按档位分组的数据:{ T1: [...], T2: [...], ... },每项为可修改 weight 的副本 */
const grouped = ref<Record<string, Array<Record<string, unknown> & { weight: number }>>>({
T1: [],
T2: [],
T3: [],
T4: [],
T5: [],
BIGWIN: []
})
function getTierItems(tier: string) {
return grouped.value[tier] ?? []
}
function getTierChartLabels(tier: string): string[] {
const items = getTierItems(tier)
return items.map((r) => (r.ui_text ? String(r.ui_text) : `点数 ${r.grid_number ?? ''}`))
}
function getTierChartData(tier: string): number[] {
const items = getTierItems(tier)
return items.map((r) => getItemWeight(r))
}
/** 用于 T1T5 校验与展示的档位权重和(仅 T1T5 使用BIGWIN 不要求合计) */
function getTierSumForValidation(tier: string): number {
const items = getTierItems(tier)
return items.reduce((s, r) => s + getItemWeight(r), 0)
}
function getItemWeight(row: Record<string, unknown> & { weight?: number }): number {
const w = row.weight
if (typeof w === 'number' && !Number.isNaN(w)) return Math.max(0, Math.min(100, w))
return 0
}
function setItemWeight(row: Record<string, unknown> & { weight: number }, value: number) {
row.weight = Math.max(0, Math.min(100, value))
}
function adjustWeight(row: Record<string, unknown> & { weight: number }, delta: number) {
const cur = getItemWeight(row)
setItemWeight(row, cur + delta)
}
/** T4、T5 及 BIGWIN 的 grid_number=5、30 不可修改权重(固定 100% */
function isWeightDisabled(row: Record<string, unknown>, tier: string): boolean {
if (tier === 'T4' || tier === 'T5') return true
if (tier === 'BIGWIN' && (row.grid_number === 5 || row.grid_number === 30)) return true
return false
}
function normalizeSliderValue(v: number | number[]): number {
if (Array.isArray(v)) return (v[0] ?? 0) as number
return (v ?? 0) as number
}
/**
* 从接口返回值中解析出按档位分组的对象。
* 兼容1) 直接返回 { T1: [], T2: [], ... } 2) 包装在 data 中 { data: { T1: [], ... } } 3) 平铺数组按 tier 分组
*/
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() {
api
.weightRatioList()
.then((res: any) => {
const data = 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) => ({
...r,
weight:
typeof r.weight === 'number' && !Number.isNaN(r.weight)
? r.weight
: Number(r.weight) || 0
}))
}
grouped.value = next
})
.catch(() => {
ElMessage.error('获取权重配比数据失败')
})
}
function validateAll(): boolean {
for (const t of TIER_KEYS) {
if (t === 'BIGWIN') continue
const items = getTierItems(t)
if (items.length === 0) continue
const sum = getTierSumForValidation(t)
if (Math.abs(sum - 100) > 0.01) {
ElMessage.warning(`档位 ${t} 的权重之和必须等于 100%,当前为 ${sum.toFixed(1)}%`)
activeTier.value = t
return false
}
}
return true
}
/**
* 收集所有档位的 id + weight同一 id 出现多次时合并权重(避免后端只保留最后一次导致档位合计不足 100%)。
* T4、T5、BIGWIN 的 5/30 不可修改,提交时固定为 100。
*/
function collectItems(): Array<{ id: number; weight: number }> {
const byId = new Map<number, number>()
for (const t of TIER_KEYS) {
for (const row of getTierItems(t)) {
const id = row.id != null ? Number(row.id) : 0
if (id >= 0 && !Number.isNaN(id)) {
const w = isWeightDisabled(row, t) ? 100 : getItemWeight(row)
byId.set(id, Math.min(100, (byId.get(id) ?? 0) + w))
}
}
}
return Array.from(byId.entries()).map(([id, weight]) => ({ id, weight }))
}
function handleSubmit() {
if (!validateAll()) return
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;
}
.weight-sum {
margin-bottom: 12px;
font-size: 13px;
.over {
color: var(--el-color-danger);
}
}
.weight-sum-bigwin {
color: var(--el-text-color-secondary);
}
.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: 100px;
padding: 0 8px;
.weight-slider {
width: 100%;
}
}
.weight-input-wrap {
display: flex;
align-items: center;
gap: 6px;
.weight-input {
width: 100px;
}
}
.empty-tip {
padding: 24px;
text-align: center;
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -6,9 +6,12 @@ DB_NAME = saiadmin
DB_USER=root DB_USER=root
DB_PASSWORD=123456 DB_PASSWORD=123456
DB_PREFIX= DB_PREFIX=
DB_POOL_MAX=32
DB_POOL_MIN=4
# 缓存方式,支持file|redisAPI 用户登录缓存需使用 redis # 缓存方式,支持file|redisAPI 用户登录缓存需使用 redis
CACHE_MODE=redis CACHE_MODE=redis
REDIS_POOL_MAX=32
# Redis配置 # Redis配置
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1

View File

@@ -5,7 +5,7 @@ namespace app\api\logic;
use app\api\cache\UserCache; use app\api\cache\UserCache;
use app\api\service\LotteryService; use app\api\service\LotteryService;
use app\dice\model\lottery_config\DiceLotteryConfig; use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
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\player_ticket_record\DicePlayerTicketRecord; use app\dice\model\player_ticket_record\DicePlayerTicketRecord;
@@ -70,19 +70,30 @@ class PlayStartLogic
$lotteryService = LotteryService::getOrCreate($playerId); $lotteryService = LotteryService::getOrCreate($playerId);
$ticketType = LotteryService::drawTicketType($paid, $free); $ticketType = LotteryService::drawTicketType($paid, $free);
$config = $ticketType === self::LOTTERY_TYPE_PAID $config = $ticketType === self::LOTTERY_TYPE_PAID
? ($lotteryService->getConfigType0Id() ? DiceLotteryConfig::find($lotteryService->getConfigType0Id()) : null) ? ($lotteryService->getConfigType0Id() ? DiceLotteryPoolConfig::find($lotteryService->getConfigType0Id()) : null)
: ($lotteryService->getConfigType1Id() ? DiceLotteryConfig::find($lotteryService->getConfigType1Id()) : null); : ($lotteryService->getConfigType1Id() ? DiceLotteryPoolConfig::find($lotteryService->getConfigType1Id()) : null);
// 未找到付费/免费对应配置时,统一回退到 type=0 的彩金池,保证所有玩家累加同一彩金池
if (!$config) {
$config = DiceLotteryPoolConfig::where('type', 0)->find();
}
if (!$config) { if (!$config) {
throw new ApiException('奖池配置不存在'); throw new ApiException('奖池配置不存在');
} }
// 按玩家权重抽取档位;若该档位无奖励或该方向下均无可用路径则重新摇取档位 // 彩金池盈利低于安全线时按玩家权重抽档,高于或等于安全线时按奖池权重抽档
$poolProfit = (float) ($config->profit_amount ?? $config->ev ?? 0);
$safetyLine = (int) ($config->safety_line ?? 0);
$usePoolWeights = $poolProfit >= $safetyLine;
// 按上述规则抽取档位;若该档位无奖励或该方向下均无可用路径则重新摇取档位
$maxTierRetry = 10; $maxTierRetry = 10;
$chosen = null; $chosen = null;
$startCandidates = []; $startCandidates = [];
$tier = null; $tier = null;
for ($tierAttempt = 0; $tierAttempt < $maxTierRetry; $tierAttempt++) { for ($tierAttempt = 0; $tierAttempt < $maxTierRetry; $tierAttempt++) {
$tier = LotteryService::drawTierByPlayerWeights($player); $tier = $usePoolWeights
? LotteryService::drawTierByWeights($config)
: LotteryService::drawTierByPlayerWeights($player);
$tierRewards = DiceRewardConfig::getCachedByTier($tier); $tierRewards = DiceRewardConfig::getCachedByTier($tier);
if (empty($tierRewards)) { if (empty($tierRewards)) {
Log::warning("档位 {$tier} 无任何奖励配置,重新摇取档位"); Log::warning("档位 {$tier} 无任何奖励配置,重新摇取档位");
@@ -122,6 +133,7 @@ class PlayStartLogic
// 当抽到的 grid_number 为 5/10/15/20/25/30 时,可出豹子;其中 grid_number=5 与 30 固定 100% 豹子BIGWIN 约定) // 当抽到的 grid_number 为 5/10/15/20/25/30 时,可出豹子;其中 grid_number=5 与 30 固定 100% 豹子BIGWIN 约定)
$superWinCoin = 0; $superWinCoin = 0;
$isWin = 0; $isWin = 0;
$bigWinRealEv = 0.0; // BIGWIN 档位的真实资金结算,用于从彩金池盈利中一并扣除
if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) { if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) {
$bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber); $bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber);
$alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true); $alwaysSuperWin = in_array($rollNumber, self::SUPER_WIN_ALWAYS_GRID_NUMBERS, true);
@@ -136,9 +148,12 @@ class PlayStartLogic
if ($doSuperWin) { if ($doSuperWin) {
$rollArray = $this->getSuperWinRollArray($rollNumber); $rollArray = $this->getSuperWinRollArray($rollNumber);
$isWin = 1; $isWin = 1;
$superWinCoin = $bigWinConfig !== null if ($bigWinConfig !== null) {
? 100 + (float) ($bigWinConfig['real_ev'] ?? 0) $bigWinRealEv = (float) ($bigWinConfig['real_ev'] ?? 0);
: self::SUPER_WIN_BONUS; $superWinCoin = 100 + $bigWinRealEv;
} else {
$superWinCoin = self::SUPER_WIN_BONUS;
}
} else { } else {
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber); $rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
} }
@@ -174,6 +189,7 @@ class PlayStartLogic
$rewardWinCoin, $rewardWinCoin,
$isWin, $isWin,
$realEv, $realEv,
$bigWinRealEv,
$direction, $direction,
$startIndex, $startIndex,
$targetIndex, $targetIndex,
@@ -230,12 +246,22 @@ class PlayStartLogic
$p->save(); $p->save();
// 累加彩金池盈利额度(累加值为 -real_ev。若 dice_lottery_config 表有 ev 字段则执行 // 彩金池盈利累加:每局累加 (100 - 本局总 real_ev),其中本局总 real_ev = 普通档位 real_ev + BIGWIN.real_ev如触发
// 需确保表有 profit_amount 字段(见 db/dice_lottery_config_add_profit_amount.sql
$totalRealEv = $realEv + $bigWinRealEv;
$addProfit = 100 - $totalRealEv;
try { try {
DiceLotteryConfig::where('id', $configId)->update([ DiceLotteryPoolConfig::where('id', $configId)->update([
'ev' => Db::raw('IFNULL(ev,0) - ' . (float) $realEv), 'profit_amount' => Db::raw('IFNULL(profit_amount,0) + ' . (float) $addProfit),
]);
} catch (\Throwable $e) {
Log::warning('彩金池盈利累加失败,请确认表 dice_lottery_config 已存在 profit_amount 字段并执行 db/dice_lottery_config_add_profit_amount.sql', [
'config_id' => $configId,
'add_profit' => $addProfit,
'real_ev' => $realEv,
'bigwin_ev' => $bigWinRealEv,
'message' => $e->getMessage(),
]); ]);
} catch (\Throwable $_) {
} }
DicePlayerWalletRecord::create([ DicePlayerWalletRecord::create([

View File

@@ -3,7 +3,7 @@ declare(strict_types=1);
namespace app\api\service; namespace app\api\service;
use app\dice\model\lottery_config\DiceLotteryConfig; use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use app\dice\model\player\DicePlayer; use app\dice\model\player\DicePlayer;
use support\think\Cache; use support\think\Cache;
@@ -37,7 +37,7 @@ class LotteryService
return self::REDIS_KEY_START_INDEX . $playerId; return self::REDIS_KEY_START_INDEX . $playerId;
} }
/** 从 Redis 加载或根据玩家与 DiceLotteryConfig 创建并保存 */ /** 从 Redis 加载或根据玩家与 DiceLotteryPoolConfig 创建并保存 */
public static function getOrCreate(int $playerId): self public static function getOrCreate(int $playerId): self
{ {
$key = self::getRedisKey($playerId); $key = self::getRedisKey($playerId);
@@ -56,8 +56,8 @@ class LotteryService
if (!$player) { if (!$player) {
throw new \RuntimeException('玩家不存在'); throw new \RuntimeException('玩家不存在');
} }
$config0 = DiceLotteryConfig::where('type', 0)->find(); $config0 = DiceLotteryPoolConfig::where('type', 0)->find();
$config1 = DiceLotteryConfig::where('type', 1)->find(); $config1 = DiceLotteryPoolConfig::where('type', 1)->find();
$s = new self($playerId); $s = new self($playerId);
$s->configType0Id = $config0 ? (int) $config0->id : null; $s->configType0Id = $config0 ? (int) $config0->id : null;
$s->configType1Id = $config1 ? (int) $config1->id : null; $s->configType1Id = $config1 ? (int) $config1->id : null;
@@ -84,7 +84,7 @@ class LotteryService
} }
/** 根据奖池配置的 t1_weight..t5_weight 权重随机抽取档位 T1-T5 */ /** 根据奖池配置的 t1_weight..t5_weight 权重随机抽取档位 T1-T5 */
public static function drawTierByWeights(DiceLotteryConfig $config): string public static function drawTierByWeights(DiceLotteryPoolConfig $config): string
{ {
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5']; $tiers = ['T1', 'T2', 'T3', 'T4', 'T5'];
$weights = [ $weights = [

View File

@@ -4,12 +4,12 @@
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
// | Author: your name // | Author: your name
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
namespace app\dice\controller\lottery_config; namespace app\dice\controller\lottery_pool_config;
use app\dice\model\lottery_config\DiceLotteryConfig; use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use plugin\saiadmin\basic\BaseController; use plugin\saiadmin\basic\BaseController;
use app\dice\logic\lottery_config\DiceLotteryConfigLogic; use app\dice\logic\lottery_pool_config\DiceLotteryPoolConfigLogic;
use app\dice\validate\lottery_config\DiceLotteryConfigValidate; use app\dice\validate\lottery_pool_config\DiceLotteryPoolConfigValidate;
use plugin\saiadmin\service\Permission; use plugin\saiadmin\service\Permission;
use support\Request; use support\Request;
use support\Response; use support\Response;
@@ -17,27 +17,27 @@ use support\Response;
/** /**
* 色子奖池配置控制器 * 色子奖池配置控制器
*/ */
class DiceLotteryConfigController extends BaseController class DiceLotteryPoolConfigController extends BaseController
{ {
/** /**
* 构造函数 * 构造函数
*/ */
public function __construct() public function __construct()
{ {
$this->logic = new DiceLotteryConfigLogic(); $this->logic = new DiceLotteryPoolConfigLogic();
$this->validate = new DiceLotteryConfigValidate; $this->validate = new DiceLotteryPoolConfigValidate;
parent::__construct(); parent::__construct();
} }
/** /**
* 获取 DiceLotteryConfig 列表数据,仅含 id、name用于 lottery_config_id 下拉(值为 id显示为 name * 获取 DiceLotteryPoolConfig 列表数据,仅含 id、name用于 lottery_config_id 下拉(值为 id显示为 name
* @param Request $request * @param Request $request
* @return Response 返回 [ ['id' => int, 'name' => string], ... ] * @return Response 返回 [ ['id' => int, 'name' => string], ... ]
*/ */
#[Permission('色子奖池配置列表', 'dice:lottery_config:index:index')] #[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
public function getOptions(Request $request): Response public function getOptions(Request $request): Response
{ {
$list = DiceLotteryConfig::field('id,name')->order('id', 'asc')->select(); $list = DiceLotteryPoolConfig::field('id,name')->order('id', 'asc')->select();
$data = $list->map(function ($item) { $data = $list->map(function ($item) {
return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')]; return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')];
})->toArray(); })->toArray();
@@ -49,7 +49,7 @@ class DiceLotteryConfigController extends BaseController
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('色子奖池配置列表', 'dice:lottery_config:index:index')] #[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
public function index(Request $request): Response public function index(Request $request): Response
{ {
$where = $request->more([ $where = $request->more([
@@ -66,7 +66,7 @@ class DiceLotteryConfigController extends BaseController
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('色子奖池配置读取', 'dice:lottery_config:index:read')] #[Permission('色子奖池配置读取', 'dice:lottery_pool_config:index:read')]
public function read(Request $request): Response public function read(Request $request): Response
{ {
$id = $request->input('id', ''); $id = $request->input('id', '');
@@ -84,7 +84,7 @@ class DiceLotteryConfigController extends BaseController
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('色子奖池配置添加', 'dice:lottery_config:index:save')] #[Permission('色子奖池配置添加', 'dice:lottery_pool_config:index:save')]
public function save(Request $request): Response public function save(Request $request): Response
{ {
$data = $request->post(); $data = $request->post();
@@ -102,7 +102,7 @@ class DiceLotteryConfigController extends BaseController
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('色子奖池配置修改', 'dice:lottery_config:index:update')] #[Permission('色子奖池配置修改', 'dice:lottery_pool_config:index:update')]
public function update(Request $request): Response public function update(Request $request): Response
{ {
$data = $request->post(); $data = $request->post();
@@ -120,7 +120,7 @@ class DiceLotteryConfigController extends BaseController
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('色子奖池配置删除', 'dice:lottery_config:index:destroy')] #[Permission('色子奖池配置删除', 'dice:lottery_pool_config:index:destroy')]
public function destroy(Request $request): Response public function destroy(Request $request): Response
{ {
$ids = $request->post('ids', ''); $ids = $request->post('ids', '');
@@ -135,4 +135,25 @@ class DiceLotteryConfigController extends BaseController
} }
} }
/**
* 获取当前彩金池Redis 实例化,无则按 type=0 创建)
* 返回含 profit_amount 实时值,供前端轮询展示
*/
#[Permission('色子奖池配置列表', 'dice:lottery_pool_config:index:index')]
public function getCurrentPool(Request $request): Response
{
$data = $this->logic->getCurrentPool();
return $this->success($data);
}
/**
* 更新当前彩金池:仅可修改 safety_line、t1_weightt5_weight不可修改 profit_amount
*/
#[Permission('色子奖池配置修改', 'dice:lottery_pool_config:index:update')]
public function updateCurrentPool(Request $request): Response
{
$data = $request->post();
$this->logic->updateCurrentPool($data);
return $this->success('保存成功');
}
} }

View File

@@ -11,7 +11,7 @@ use plugin\saiadmin\basic\BaseController;
use app\dice\logic\play_record\DicePlayRecordLogic; 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_config\DiceLotteryConfig; use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use app\dice\model\reward_config\DiceRewardConfig; use app\dice\model\reward_config\DiceRewardConfig;
use plugin\saiadmin\service\Permission; use plugin\saiadmin\service\Permission;
use support\Request; use support\Request;
@@ -58,7 +58,7 @@ class DicePlayRecordController extends BaseController
$query->with([ $query->with([
'dicePlayer', 'dicePlayer',
'diceRewardConfig', 'diceRewardConfig',
'diceLotteryConfig', 'diceLotteryPoolConfig',
]); ]);
$data = $this->logic->getList($query); $data = $this->logic->getList($query);
return $this->success($data); return $this->success($data);
@@ -85,7 +85,7 @@ class DicePlayRecordController extends BaseController
#[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')] #[Permission('玩家抽奖记录列表', 'dice:play_record:index:index')]
public function getLotteryConfigOptions(Request $request): Response public function getLotteryConfigOptions(Request $request): Response
{ {
$list = DiceLotteryConfig::field('id,name')->select(); $list = DiceLotteryPoolConfig::field('id,name')->select();
$data = $list->map(function ($item) { $data = $list->map(function ($item) {
return ['id' => $item['id'], 'name' => $item['name'] ?? '']; return ['id' => $item['id'], 'name' => $item['name'] ?? ''];
})->toArray(); })->toArray();

View File

@@ -1,13 +1,13 @@
<?php <?php
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ] // | saiadmin [ saiadmin?????? ]
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
// | Author: your name // | Author: your name
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
namespace app\dice\controller\player; namespace app\dice\controller\player;
use app\dice\helper\AdminScopeHelper; use app\dice\helper\AdminScopeHelper;
use app\dice\model\lottery_config\DiceLotteryConfig; use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use plugin\saiadmin\app\model\system\SystemUser; use plugin\saiadmin\app\model\system\SystemUser;
use plugin\saiadmin\basic\BaseController; use plugin\saiadmin\basic\BaseController;
use app\dice\logic\player\DicePlayerLogic; use app\dice\logic\player\DicePlayerLogic;
@@ -17,12 +17,12 @@ use support\Request;
use support\Response; use support\Response;
/** /**
* 大富翁-玩家控制器 * ???-?????
*/ */
class DicePlayerController extends BaseController class DicePlayerController extends BaseController
{ {
/** /**
* 构造函数 * ????
*/ */
public function __construct() public function __construct()
{ {
@@ -32,14 +32,14 @@ class DicePlayerController extends BaseController
} }
/** /**
* 获取彩金池配置选项(DiceLotteryConfig.idname),供前端 lottery_config_id 下拉使用 * ??????????DiceLotteryPoolConfig.id?name????? lottery_config_id ????
* @param Request $request * @param Request $request
* @return Response 返回 [ ['id' => int, 'name' => string], ... ] * @return Response ?? [ ['id' => int, 'name' => string], ... ]
*/ */
#[Permission('大富翁-玩家列表', 'dice:player:index:index')] #[Permission('???-????', 'dice:player:index:index')]
public function getLotteryConfigOptions(Request $request): Response public function getLotteryConfigOptions(Request $request): Response
{ {
$list = DiceLotteryConfig::field('id,name')->order('id', 'asc')->select(); $list = DiceLotteryPoolConfig::field('id,name')->order('id', 'asc')->select();
$data = $list->map(function ($item) { $data = $list->map(function ($item) {
return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')]; return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')];
})->toArray(); })->toArray();
@@ -47,12 +47,12 @@ class DicePlayerController extends BaseController
} }
/** /**
* 获取后台管理员选项(SystemUser.idusernamerealname),供 admin_id 下拉使用 * ??????????SystemUser.id?username?realname??? admin_id ????
* 根据当前登录用户权限过滤(超级管理员可见全部,普通管理员按部门) * ????????????????????????????????
* @param Request $request * @param Request $request
* @return Response 返回 [ ['id' => int, 'username' => string, 'realname' => string], ... ] * @return Response ?? [ ['id' => int, 'username' => string, 'realname' => string], ... ]
*/ */
#[Permission('大富翁-玩家列表', 'dice:player:index:index')] #[Permission('???-????', 'dice:player:index:index')]
public function getSystemUserOptions(Request $request): Response public function getSystemUserOptions(Request $request): Response
{ {
$query = SystemUser::field('id,username,realname')->where('status', 1)->order('id', 'asc'); $query = SystemUser::field('id,username,realname')->where('status', 1)->order('id', 'asc');
@@ -76,11 +76,11 @@ class DicePlayerController extends BaseController
} }
/** /**
* 数据列表 * ????
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('大富翁-玩家列表', 'dice:player:index:index')] #[Permission('???-????', 'dice:player:index:index')]
public function index(Request $request): Response public function index(Request $request): Response
{ {
$where = $request->more([ $where = $request->more([
@@ -93,60 +93,60 @@ class DicePlayerController extends BaseController
]); ]);
$query = $this->logic->search($where); $query = $this->logic->search($where);
AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null); AdminScopeHelper::applyAdminScope($query, $this->adminInfo ?? null);
$query->with(['diceLotteryConfig']); $query->with(['diceLotteryPoolConfig']);
$data = $this->logic->getList($query); $data = $this->logic->getList($query);
return $this->success($data); return $this->success($data);
} }
/** /**
* 读取数据 * ????
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('大富翁-玩家读取', 'dice:player:index:read')] #[Permission('???-????', 'dice:player:index:read')]
public function read(Request $request): Response public function read(Request $request): Response
{ {
$id = $request->input('id', ''); $id = $request->input('id', '');
$model = $this->logic->read($id); $model = $this->logic->read($id);
if (!$model) { if (!$model) {
return $this->fail('未查找到信息'); return $this->fail('??????');
} }
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null); $allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) { if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限查看该玩家'); return $this->fail('????????');
} }
$data = is_array($model) ? $model : $model->toArray(); $data = is_array($model) ? $model : $model->toArray();
return $this->success($data); return $this->success($data);
} }
/** /**
* 保存数据 * ????
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('大富翁-玩家添加', 'dice:player:index:save')] #[Permission('???-????', 'dice:player:index:save')]
public function save(Request $request): Response public function save(Request $request): Response
{ {
$data = $request->post(); $data = $request->post();
$this->validate('save', $data); $this->validate('save', $data);
// 新增时若未选择管理员,默认使用当前登录用户 // ?????????????????????
if (empty($data['admin_id']) && isset($this->adminInfo['id']) && (int) $this->adminInfo['id'] > 0) { if (empty($data['admin_id']) && isset($this->adminInfo['id']) && (int) $this->adminInfo['id'] > 0) {
$data['admin_id'] = (int) $this->adminInfo['id']; $data['admin_id'] = (int) $this->adminInfo['id'];
} }
$result = $this->logic->add($data); $result = $this->logic->add($data);
if ($result) { if ($result) {
return $this->success('添加成功'); return $this->success('????');
} else { } else {
return $this->fail('添加失败'); return $this->fail('????');
} }
} }
/** /**
* 更新数据 * ????
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('大富翁-玩家修改', 'dice:player:index:update')] #[Permission('???-????', 'dice:player:index:update')]
public function update(Request $request): Response public function update(Request $request): Response
{ {
$data = $request->post(); $data = $request->post();
@@ -155,55 +155,55 @@ class DicePlayerController extends BaseController
if ($model) { if ($model) {
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null); $allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) { if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限修改该玩家'); return $this->fail('????????');
} }
} }
$result = $this->logic->edit($data['id'], $data); $result = $this->logic->edit($data['id'], $data);
if ($result) { if ($result) {
return $this->success('修改成功'); return $this->success('????');
} else { } else {
return $this->fail('修改失败'); return $this->fail('????');
} }
} }
/** /**
* 仅更新状态(列表内开关用) * ?????????????
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('大富翁-玩家修改', 'dice:player:index:update')] #[Permission('???-????', 'dice:player:index:update')]
public function updateStatus(Request $request): Response public function updateStatus(Request $request): Response
{ {
$id = $request->input('id'); $id = $request->input('id');
$status = $request->input('status'); $status = $request->input('status');
if ($id === null || $id === '') { if ($id === null || $id === '') {
return $this->fail('缺少 id'); return $this->fail('?? id');
} }
if ($status === null || $status === '') { if ($status === null || $status === '') {
return $this->fail('缺少 status'); return $this->fail('?? status');
} }
$model = $this->logic->read($id); $model = $this->logic->read($id);
if ($model) { if ($model) {
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null); $allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) { if ($allowedIds !== null && !in_array((int) ($model->admin_id ?? 0), $allowedIds, true)) {
return $this->fail('无权限修改该玩家'); return $this->fail('????????');
} }
} }
$this->logic->edit($id, ['status' => (int) $status]); $this->logic->edit($id, ['status' => (int) $status]);
return $this->success('修改成功'); return $this->success('????');
} }
/** /**
* 删除数据 * ????
* @param Request $request * @param Request $request
* @return Response * @return Response
*/ */
#[Permission('大富翁-玩家删除', 'dice:player:index:destroy')] #[Permission('???-????', 'dice:player:index:destroy')]
public function destroy(Request $request): Response public function destroy(Request $request): Response
{ {
$ids = $request->post('ids', ''); $ids = $request->post('ids', '');
if (empty($ids)) { if (empty($ids)) {
return $this->fail('请选择要删除的数据'); return $this->fail('?????????');
} }
$ids = is_array($ids) ? $ids : explode(',', (string) $ids); $ids = is_array($ids) ? $ids : explode(',', (string) $ids);
$allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null); $allowedIds = AdminScopeHelper::getAllowedAdminIds($this->adminInfo ?? null);
@@ -218,14 +218,14 @@ class DicePlayerController extends BaseController
} }
$ids = $validIds; $ids = $validIds;
if (empty($ids)) { if (empty($ids)) {
return $this->fail('无权限删除所选玩家'); return $this->fail('?????????');
} }
} }
$result = $this->logic->destroy($ids); $result = $this->logic->destroy($ids);
if ($result) { if ($result) {
return $this->success('删除成功'); return $this->success('????');
} else { } else {
return $this->fail('删除失败'); return $this->fail('????');
} }
} }

View File

@@ -123,4 +123,35 @@ class DiceRewardConfigController extends BaseController
} }
} }
/**
* T1-T5、BIGWIN 权重配比:按档位分组返回配置列表
* @param Request $request
* @return Response
*/
#[Permission('奖励配置列表', 'dice:reward_config:index:index')]
public function weightRatioList(Request $request): Response
{
$data = $this->logic->getListGroupedByTier();
return $this->success($data);
}
/**
* T1-T5、BIGWIN 权重配比批量更新权重T1-T5 同档位权重和须为 100%BIGWIN 为豹子权重单独设定、无合计要求)
* @param Request $request
* @return Response
*/
#[Permission('奖励配置修改', 'dice:reward_config:index:update')]
public function batchUpdateWeights(Request $request): Response
{
$items = $request->post('items', []);
if (!is_array($items)) {
return $this->fail('参数 items 必须为数组');
}
try {
$this->logic->batchUpdateWeights($items);
return $this->success('保存成功');
} catch (\plugin\saiadmin\exception\ApiException $e) {
return $this->fail($e->getMessage());
}
}
} }

View File

@@ -1,27 +0,0 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\logic\lottery_config;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use app\dice\model\lottery_config\DiceLotteryConfig;
/**
* 色子奖池配置逻辑层
*/
class DiceLotteryConfigLogic extends BaseLogic
{
/**
* 构造函数
*/
public function __construct()
{
$this->model = new DiceLotteryConfig();
}
}

View File

@@ -0,0 +1,112 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
// | Author: your name
// +----------------------------------------------------------------------
namespace app\dice\logic\lottery_pool_config;
use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
use plugin\saiadmin\basic\think\BaseLogic;
use plugin\saiadmin\exception\ApiException;
use plugin\saiadmin\utils\Helper;
use support\think\Cache;
/**
* 色子奖池配置逻辑层
*/
class DiceLotteryPoolConfigLogic extends BaseLogic
{
/** Redis 当前彩金池type=0 实例key无则按 type=0 创建 */
private const REDIS_KEY_CURRENT_POOL = 'api:game:lottery_pool:default';
private const EXPIRE = 86400 * 7;
/**
* 构造函数
*/
public function __construct()
{
$this->model = new DiceLotteryPoolConfig();
}
/**
* 获取当前彩金池:从 Redis 读取实例profit_amount 每次从 DB 实时读取以保证与抽奖累加一致
*
* @return array{id:int,name:string,safety_line:int,t1_weight:int,t2_weight:int,t3_weight:int,t4_weight:int,t5_weight:int,profit_amount:float}
*/
public function getCurrentPool(): array
{
$cached = Cache::get(self::REDIS_KEY_CURRENT_POOL);
if ($cached && is_string($cached)) {
$data = json_decode($cached, true);
if (is_array($data)) {
$config = DiceLotteryPoolConfig::find($data['id'] ?? 0);
$profit = 0.0;
if ($config) {
$profit = isset($config->profit_amount) ? (float) $config->profit_amount : (isset($config->ev) ? (float) $config->ev : 0.0);
} else {
$profit = (float) ($data['profit_amount'] ?? 0);
}
$data['profit_amount'] = $profit;
return $data;
}
}
$config = DiceLotteryPoolConfig::where('type', 0)->find();
if (!$config) {
throw new ApiException('未找到 type=0 的奖池配置,请先创建');
}
$row = $config->toArray();
$profitAmount = isset($row['profit_amount']) ? (float) $row['profit_amount'] : (isset($row['ev']) ? (float) $row['ev'] : 0.0);
$pool = [
'id' => (int) $row['id'],
'name' => (string) ($row['name'] ?? ''),
'safety_line' => (int) ($row['safety_line'] ?? 0),
't1_weight' => (int) ($row['t1_weight'] ?? 0),
't2_weight' => (int) ($row['t2_weight'] ?? 0),
't3_weight' => (int) ($row['t3_weight'] ?? 0),
't4_weight' => (int) ($row['t4_weight'] ?? 0),
't5_weight' => (int) ($row['t5_weight'] ?? 0),
'profit_amount' => $profitAmount,
];
Cache::set(self::REDIS_KEY_CURRENT_POOL, json_encode($pool), self::EXPIRE);
return $pool;
}
/**
* 更新当前彩金池:仅允许修改 safety_line、t1_weightt5_weight不修改 profit_amount
* 同时更新 Redis 与 DB 中 type=0 的记录
*
* @param array{safety_line?:int,t1_weight?:int,t2_weight?:int,t3_weight?:int,t4_weight?:int,t5_weight?:int} $data
*/
public function updateCurrentPool(array $data): void
{
$pool = $this->getCurrentPool();
$id = (int) $pool['id'];
$config = DiceLotteryPoolConfig::find($id);
if (!$config) {
throw new ApiException('奖池配置不存在');
}
$allow = ['safety_line', 't1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight'];
$update = [];
foreach ($allow as $k) {
if (array_key_exists($k, $data)) {
if ($k === 'safety_line') {
$update[$k] = (int) $data[$k];
} else {
$update[$k] = max(0, min(100, (int) $data[$k]));
}
}
}
if (empty($update)) {
return;
}
DiceLotteryPoolConfig::where('id', $id)->update($update);
$pool = array_merge($pool, $update);
$refreshed = DiceLotteryPoolConfig::find($id);
$pool['profit_amount'] = $refreshed && (isset($refreshed->profit_amount) || isset($refreshed->ev))
? (float) ($refreshed->profit_amount ?? $refreshed->ev)
: (float) ($pool['profit_amount'] ?? 0);
Cache::set(self::REDIS_KEY_CURRENT_POOL, json_encode($pool), self::EXPIRE);
}
}

View File

@@ -10,6 +10,7 @@ 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;
use app\dice\model\reward_config\DiceRewardConfig; use app\dice\model\reward_config\DiceRewardConfig;
use support\Log;
/** /**
* 奖励配置逻辑层 * 奖励配置逻辑层
@@ -57,4 +58,85 @@ class DiceRewardConfigLogic extends BaseLogic
$data['weight'] = max(0, min(100, $w)); $data['weight'] = max(0, min(100, $w));
return $data; return $data;
} }
/**
* 按档位分组返回奖励配置列表(用于 T1-T5、BIGWIN 权重配比)
* @return array<string, array> 键为 T1|T2|T3|T4|T5|BIGWIN值为该档位下的配置行数组
*/
public function getListGroupedByTier(): array
{
$tiers = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'];
$list = $this->model->whereIn('tier', $tiers)->order('tier')->order('id')->select()->toArray();
$grouped = [];
foreach ($tiers as $t) {
$grouped[$t] = [];
}
foreach ($list as $row) {
$tier = isset($row['tier']) ? (string) $row['tier'] : '';
if ($tier !== '' && isset($grouped[$tier])) {
$grouped[$tier][] = $row;
}
}
return $grouped;
}
/**
* 批量更新权重T1-T5 同档位权重之和必须等于 100BIGWIN 为豹子权重单独设定,不校验合计
* @param array<int, array{id: int, weight: float}> $items 元素为 [ id => 配置ID, weight => 0-100 ]
* @throws ApiException 当单条 weight 非法或 T1-T5 某档位权重和≠100 时
*/
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']) ? (float) $item['weight'] : 0;
if ($id < 0) {
throw new ApiException('存在无效的配置ID');
}
if ($w < 0 || $w > 100) {
throw new ApiException('权重必须在 0-100 之间');
}
$ids[] = $id;
$w = max(0, min(100, $w));
$weightById[$id] = isset($weightById[$id]) ? min(100, $weightById[$id] + $w) : $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'] : '';
}
$sumByTier = [];
foreach ($weightById as $id => $w) {
$tier = $idToTier[$id] ?? '';
if ($tier === '') {
throw new ApiException('配置ID ' . $id . ' 不存在或档位为空');
}
if ($tier === 'BIGWIN') {
continue;
}
if (!isset($sumByTier[$tier])) {
$sumByTier[$tier] = 0;
}
$sumByTier[$tier] += $w;
}
foreach ($sumByTier as $tier => $sum) {
if (abs($sum - 100)) {
throw new ApiException('档位 ' . $tier . ' 的权重之和必须等于 100%,当前为 ' . round($sum, 2));
}
}
foreach ($weightById as $id => $w) {
DiceRewardConfig::where('id', $id)->update(['weight' => $w]);
}
DiceRewardConfig::refreshCache();
}
} }

View File

@@ -4,7 +4,7 @@
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
// | Author: your name // | Author: your name
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
namespace app\dice\model\lottery_config; namespace app\dice\model\lottery_pool_config;
use plugin\saiadmin\basic\think\BaseModel; use plugin\saiadmin\basic\think\BaseModel;
@@ -25,8 +25,9 @@ use plugin\saiadmin\basic\think\BaseModel;
* @property $t3_weight T3池权重 * @property $t3_weight T3池权重
* @property $t4_weight T4池权重 * @property $t4_weight T4池权重
* @property $t5_weight T5池权重 * @property $t5_weight T5池权重
* @property $profit_amount 池子累计盈利(每局抽奖累加 100-real_ev仅展示不可编辑
*/ */
class DiceLotteryConfig extends BaseModel class DiceLotteryPoolConfig extends BaseModel
{ {
/** /**
* 数据表主键 * 数据表主键

View File

@@ -6,7 +6,7 @@
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
namespace app\dice\model\play_record; namespace app\dice\model\play_record;
use app\dice\model\lottery_config\DiceLotteryConfig; 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_config\DiceRewardConfig;
use plugin\saiadmin\basic\think\BaseModel; use plugin\saiadmin\basic\think\BaseModel;
@@ -71,12 +71,12 @@ class DicePlayRecord extends BaseModel
} }
/** /**
* 彩金配置 * 彩金配置
* 关联模型 diceLotteryConfig * 关联模型 diceLotteryPoolConfig
*/ */
public function diceLotteryConfig(): BelongsTo public function diceLotteryPoolConfig(): BelongsTo
{ {
return $this->belongsTo(DiceLotteryConfig::class, 'lottery_config_id', 'id'); return $this->belongsTo(DiceLotteryPoolConfig::class, 'lottery_config_id', 'id');
} }
/** 按玩家用户名模糊dicePlayer.username */ /** 按玩家用户名模糊dicePlayer.username */
@@ -93,13 +93,13 @@ class DicePlayRecord extends BaseModel
} }
} }
/** 按彩金池配置名称模糊diceLotteryConfig.name */ /** 按彩金池配置名称模糊diceLotteryPoolConfig.name */
public function searchLotteryConfigNameAttr($query, $value) public function searchLotteryConfigNameAttr($query, $value)
{ {
if ($value === '' || $value === null) { if ($value === '' || $value === null) {
return; return;
} }
$ids = DiceLotteryConfig::where('name', 'like', '%' . $value . '%')->column('id'); $ids = DiceLotteryPoolConfig::where('name', 'like', '%' . $value . '%')->column('id');
if (!empty($ids)) { if (!empty($ids)) {
$query->whereIn('lottery_config_id', $ids); $query->whereIn('lottery_config_id', $ids);
} else { } else {

View File

@@ -7,7 +7,7 @@
namespace app\dice\model\player; namespace app\dice\model\player;
use plugin\saiadmin\basic\think\BaseModel; use plugin\saiadmin\basic\think\BaseModel;
use app\dice\model\lottery_config\DiceLotteryConfig; use app\dice\model\lottery_pool_config\DiceLotteryPoolConfig;
/** /**
* 大富翁-玩家模型 * 大富翁-玩家模型
@@ -78,14 +78,14 @@ class DicePlayer extends BaseModel
if ($name === null || $name === '') { if ($name === null || $name === '') {
$model->setAttr('name', $uid); $model->setAttr('name', $uid);
} }
// 创建玩家时:未指定则自动保存 lottery_config_id 为 DiceLotteryConfig type=0 的 id没有则为 0 // 创建玩家时:未指定则自动保存 lottery_config_id 为 DiceLotteryPoolConfig type=0 的 id没有则为 0
try { try {
$lotteryConfigId = $model->getAttr('lottery_config_id'); $lotteryConfigId = $model->getAttr('lottery_config_id');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$lotteryConfigId = null; $lotteryConfigId = null;
} }
if ($lotteryConfigId === null || $lotteryConfigId === '' || (int) $lotteryConfigId === 0) { if ($lotteryConfigId === null || $lotteryConfigId === '' || (int) $lotteryConfigId === 0) {
$config = DiceLotteryConfig::where('type', 0)->find(); $config = DiceLotteryPoolConfig::where('type', 0)->find();
$model->setAttr('lottery_config_id', $config ? (int) $config->id : 0); $model->setAttr('lottery_config_id', $config ? (int) $config->id : 0);
} }
// 彩金池权重默认取 type=0 的奖池配置 // 彩金池权重默认取 type=0 的奖池配置
@@ -93,11 +93,11 @@ class DicePlayer extends BaseModel
} }
/** /**
* 从 DiceLotteryConfig type=0 取 t1_weightt5_weight 作为玩家未设置时的默认值 * 从 DiceLotteryPoolConfig type=0 取 t1_weightt5_weight 作为玩家未设置时的默认值
*/ */
protected static function setDefaultWeightsFromLotteryConfig(DicePlayer $model): void protected static function setDefaultWeightsFromLotteryConfig(DicePlayer $model): void
{ {
$config = DiceLotteryConfig::where('type', 0)->find(); $config = DiceLotteryPoolConfig::where('type', 0)->find();
if (!$config) { if (!$config) {
return; return;
} }
@@ -185,8 +185,8 @@ class DicePlayer extends BaseModel
/** /**
* 关联彩金池配置 * 关联彩金池配置
*/ */
public function diceLotteryConfig() public function diceLotteryPoolConfig()
{ {
return $this->belongsTo(DiceLotteryConfig::class, 'lottery_config_id', 'id'); return $this->belongsTo(DiceLotteryPoolConfig::class, 'lottery_config_id', 'id');
} }
} }

View File

@@ -110,20 +110,16 @@ class DiceRewardConfig extends BaseModel
} }
} }
$sEnd = isset($row['s_end_index']) ? (int) $row['s_end_index'] : 0; $sEnd = isset($row['s_end_index']) ? (int) $row['s_end_index'] : 0;
if ($sEnd !== 0) {
if (!isset($bySEndIndex[$sEnd])) { if (!isset($bySEndIndex[$sEnd])) {
$bySEndIndex[$sEnd] = []; $bySEndIndex[$sEnd] = [];
} }
$bySEndIndex[$sEnd][] = $row; $bySEndIndex[$sEnd][] = $row;
}
$nEnd = isset($row['n_end_index']) ? (int) $row['n_end_index'] : 0; $nEnd = isset($row['n_end_index']) ? (int) $row['n_end_index'] : 0;
if ($nEnd !== 0) {
if (!isset($byNEndIndex[$nEnd])) { if (!isset($byNEndIndex[$nEnd])) {
$byNEndIndex[$nEnd] = []; $byNEndIndex[$nEnd] = [];
} }
$byNEndIndex[$nEnd][] = $row; $byNEndIndex[$nEnd][] = $row;
} }
}
$minRealEv = empty($list) ? 0.0 : (float) min(array_column($list, 'real_ev')); $minRealEv = empty($list) ? 0.0 : (float) min(array_column($list, 'real_ev'));
self::$instance = [ self::$instance = [
'list' => $list, 'list' => $list,

View File

@@ -4,14 +4,14 @@
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
// | Author: your name // | Author: your name
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
namespace app\dice\validate\lottery_config; namespace app\dice\validate\lottery_pool_config;
use plugin\saiadmin\basic\BaseValidate; use plugin\saiadmin\basic\BaseValidate;
/** /**
* 色子奖池配置验证器 * 色子奖池配置验证器
*/ */
class DiceLotteryConfigValidate extends BaseValidate class DiceLotteryPoolConfigValidate extends BaseValidate
{ {
/** /**
* 定义验证规则 * 定义验证规则

View File

@@ -18,7 +18,7 @@ abstract class AbstractLogic implements LogicInterface
* 模型注入 * 模型注入
* @var object * @var object
*/ */
protected $model; public $model;
/** /**
* 管理员信息 * 管理员信息

View File

@@ -103,8 +103,12 @@ Route::group('/core', function () {
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_config/DiceRewardConfig', \app\dice\controller\reward_config\DiceRewardConfigController::class); fastRoute('dice/reward_config/DiceRewardConfig', \app\dice\controller\reward_config\DiceRewardConfigController::class);
fastRoute('dice/lottery_config/DiceLotteryConfig', \app\dice\controller\lottery_config\DiceLotteryConfigController::class); Route::get('/dice/reward_config/DiceRewardConfig/weightRatioList', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'weightRatioList']);
Route::get('/dice/lottery_config/DiceLotteryConfig/getOptions', [\app\dice\controller\lottery_config\DiceLotteryConfigController::class, 'getOptions']); Route::post('/dice/reward_config/DiceRewardConfig/batchUpdateWeights', [\app\dice\controller\reward_config\DiceRewardConfigController::class, 'batchUpdateWeights']);
fastRoute('dice/lottery_pool_config/DiceLotteryPoolConfig', \app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class);
Route::get('/dice/lottery_pool_config/DiceLotteryPoolConfig/getOptions', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'getOptions']);
Route::get('/dice/lottery_pool_config/DiceLotteryPoolConfig/getCurrentPool', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'getCurrentPool']);
Route::post('/dice/lottery_pool_config/DiceLotteryPoolConfig/updateCurrentPool', [\app\dice\controller\lottery_pool_config\DiceLotteryPoolConfigController::class, 'updateCurrentPool']);
// 数据表维护 // 数据表维护
Route::get("/database/index", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'index']); Route::get("/database/index", [\plugin\saiadmin\app\controller\system\DataBaseController::class, 'index']);