Compare commits

...

3 Commits

Author SHA1 Message Date
6632923213 优化游玩记录DicePlayRecord 2026-03-07 14:40:33 +08:00
316506b597 优化玩家DicePlayer保存的lottery_config_id 2026-03-07 11:58:47 +08:00
282d73a203 优化玩家DicePlayer权重输入方式 2026-03-07 11:51:34 +08:00
21 changed files with 540 additions and 90 deletions

View File

@@ -17,17 +17,16 @@ export default {
}, },
/** /**
* 获取彩金池配置下拉选项(来自 DiceLotteryConfig用于 lottery_config_id 选择) * 获取 DiceLotteryConfig 列表数据,仅含 id、name,用于 lottery_config_id 下拉
* @returns { id, name }[] * @returns DiceLotteryConfig['id','name'] 列表
*/ */
async getOptions(): Promise<Array<{ id: number; name: string }>> { async getOptions(): Promise<Array<{ id: number; name: string }>> {
const res = await request.get<any>({ const res = await request.get<any>({
url: '/dice/lottery_config/DiceLotteryConfig/index', url: '/dice/lottery_config/DiceLotteryConfig/getOptions'
params: { limit: 500, page: 1 }
}) })
const rows = (res?.data?.data ?? res?.data?.rows ?? []) as Array<{ id: number; name: string }> const rows = (res?.data ?? []) as Array<{ id: number; name: string }>
return Array.isArray(rows) return Array.isArray(rows)
? rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id) })) ? rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') }))
: [] : []
}, },

View File

@@ -71,5 +71,17 @@ export default {
url: '/dice/player/DicePlayer/updateStatus', url: '/dice/player/DicePlayer/updateStatus',
data: params data: params
}) })
},
/**
* 获取彩金池配置选项DiceLotteryConfig.id、name供 lottery_config_id 下拉使用
* @returns [ { id, name } ]
*/
async getLotteryConfigOptions(): Promise<Array<{ id: number; name: string }>> {
const res = await request.get<any>({
url: '/dice/player/DicePlayer/getLotteryConfigOptions'
})
const rows = (Array.isArray(res) ? res : (res?.data ?? [])) as Array<{ id: number; name: string }>
return rows.map((r) => ({ id: Number(r.id), name: String(r.name ?? r.id ?? '') }))
} }
} }

View File

@@ -117,6 +117,8 @@
is_win: undefined, is_win: undefined,
win_coin_min: undefined, win_coin_min: undefined,
win_coin_max: undefined, win_coin_max: undefined,
roll_number_min: undefined,
roll_number_max: undefined,
reward_ui_text: undefined, reward_ui_text: undefined,
reward_tier: undefined, reward_tier: undefined,
direction: undefined direction: undefined
@@ -190,6 +192,7 @@
{ prop: 'start_index', label: '起始索引', width: 90 }, { prop: 'start_index', label: '起始索引', width: 90 },
{ prop: 'target_index', label: '终点索引', width: 90 }, { prop: 'target_index', label: '终点索引', width: 90 },
{ prop: 'roll_array', label: '摇取点数', width: 140, useSlot: true }, { prop: 'roll_array', label: '摇取点数', width: 140, useSlot: true },
{ prop: 'roll_number', label: '摇取点数和', width: 110, sortable: true },
{ {
prop: 'reward_config_id', prop: 'reward_config_id',
label: '奖励配置', label: '奖励配置',

View File

@@ -142,6 +142,17 @@
</div> </div>
<div class="roll-array-hint">固定 5 个数每个 16</div> <div class="roll-array-hint">固定 5 个数每个 16</div>
</el-form-item> </el-form-item>
<el-form-item label="摇取点数和" prop="roll_number">
<el-input-number
v-model="formData.roll_number"
placeholder="5 个色子点数之和530"
:min="5"
:max="30"
:precision="0"
style="width: 100%"
:disabled="dialogType === 'edit'"
/>
</el-form-item>
<el-form-item label="奖励配置" prop="reward_config_id"> <el-form-item label="奖励配置" prop="reward_config_id">
<el-select <el-select
v-model="formData.reward_config_id" v-model="formData.reward_config_id"
@@ -245,6 +256,7 @@
start_index: null as number | null, start_index: null as number | null,
target_index: null as number | null, target_index: null as number | null,
roll_array: null as string | number[] | null, roll_array: null as string | number[] | null,
roll_number: null as number | null,
reward_config_id: null as number | null reward_config_id: null as number | null
} }
@@ -306,6 +318,7 @@
'start_index', 'start_index',
'target_index', 'target_index',
'roll_array', 'roll_array',
'roll_number',
'reward_config_id' 'reward_config_id'
] ]
keys.forEach((key) => { keys.forEach((key) => {
@@ -319,6 +332,10 @@
} }
} }
}) })
// 若后端未返回 roll_number根据摇取点数计算
if (formData.roll_number == null && formData.rollArrayItems.length === 5) {
formData.roll_number = formData.rollArrayItems.reduce((s, n) => s + (n ?? 0), 0) || null
}
} }
/** 将接口的 roll_array 转为固定 5 项数组,不足补 null */ /** 将接口的 roll_array 转为固定 5 项数组,不足补 null */
@@ -355,10 +372,12 @@
const payload = { ...formData } as Record<string, unknown> const payload = { ...formData } as Record<string, unknown>
// 将 5 个输入值拼成 [1,2,3,4,5] 格式,确保每项为 16 的整数 // 将 5 个输入值拼成 [1,2,3,4,5] 格式,确保每项为 16 的整数
const items = formData.rollArrayItems const items = formData.rollArrayItems
payload.roll_array = items.map((n) => { const rollArray = items.map((n) => {
const v = n != null ? Number(n) : 1 const v = n != null ? Number(n) : 1
return Math.min(6, Math.max(1, Number.isNaN(v) ? 1 : Math.floor(v))) return Math.min(6, Math.max(1, Number.isNaN(v) ? 1 : Math.floor(v)))
}) })
payload.roll_array = rollArray
payload.roll_number = formData.roll_number ?? rollArray.reduce((s, n) => s + n, 0)
delete payload.rollArrayItems delete payload.rollArrayItems
if (props.dialogType === 'add') { if (props.dialogType === 'add') {
delete payload.id delete payload.id

View File

@@ -63,6 +63,31 @@
</div> </div>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col v-bind="setSpan(6)">
<el-form-item label="摇取点数和" prop="roll_number_min">
<div class="range-wrap">
<el-input-number
v-model="formData.roll_number_min"
placeholder="最小"
:min="5"
:max="30"
:precision="0"
controls-position="right"
class="range-input"
/>
<span class="range-sep"></span>
<el-input-number
v-model="formData.roll_number_max"
placeholder="最大"
:min="5"
:max="30"
:precision="0"
controls-position="right"
class="range-input"
/>
</div>
</el-form-item>
</el-col>
<el-col v-bind="setSpan(6)"> <el-col v-bind="setSpan(6)">
<el-form-item label="奖励配置" prop="reward_ui_text"> <el-form-item label="奖励配置" prop="reward_ui_text">
<el-input v-model="formData.reward_ui_text" placeholder="前端显示文本模糊" clearable /> <el-input v-model="formData.reward_ui_text" placeholder="前端显示文本模糊" clearable />

View File

@@ -44,39 +44,98 @@
style="width: 100%" style="width: 100%"
/> />
</el-form-item> </el-form-item>
<!-- lottery_config_id = 自定义权重否则 = DiceLotteryConfig.id选择后该配置的五个 weight 会写入下方 player.*_weight -->
<el-form-item label="彩金池配置" prop="lottery_config_id"> <el-form-item label="彩金池配置" prop="lottery_config_id">
<el-select <el-select
v-model="formData.lottery_config_id" v-model="formData.lottery_config_id"
placeholder="不选则使用下方自定义权重" placeholder="留空则使用下方自定义权重,或选择彩金池"
clearable clearable
filterable
style="width: 100%" style="width: 100%"
:loading="lotteryConfigLoading"
@change="onLotteryConfigChange" @change="onLotteryConfigChange"
> >
<el-option label="自定义权重" :value="0" />
<el-option <el-option
v-for="item in lotteryConfigOptions" v-for="item in lotteryConfigOptions"
:key="item.id" :key="item.id"
:label="item.name" :label="(item.name && String(item.name).trim()) || `#${item.id}`"
:value="item.id" :value="item.id"
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="!formData.lottery_config_id" label="T1池权重(%)" prop="t1_weight"> <!-- 当前选中的 DiceLotteryConfig 数据展示 -->
<el-slider v-model="formData.t1_weight" :min="0" :max="100" :step="0.01" show-input /> <el-form-item v-if="currentLotteryConfig" label="当前配置" class="current-config-block">
<div class="current-lottery-config">
<div class="config-row">
<span class="config-label">名称</span>
<span>{{ currentLotteryConfig.name ?? '-' }}</span>
</div>
<div class="config-row">
<span class="config-label">类型</span>
<span>{{ lotteryConfigTypeText(currentLotteryConfig.type) }}</span>
</div>
<div class="config-row">
<span class="config-label">T1T5 权重</span>
<span>{{ currentLotteryConfigWeightsText }}</span>
</div>
<div v-if="currentLotteryConfig.remark" class="config-row">
<span class="config-label">备注</span>
<span>{{ currentLotteryConfig.remark }}</span>
</div>
</div>
</el-form-item> </el-form-item>
<el-form-item v-if="!formData.lottery_config_id" label="T2池权重(%)" prop="t2_weight"> <!-- lottery_config_id 为空时自定义权重可编辑有值时来自所选 DiceLotteryConfig仅展示不可编辑 -->
<el-slider v-model="formData.t2_weight" :min="0" :max="100" :step="0.01" show-input /> <el-form-item label="T1池权重(%)" prop="t1_weight">
<el-slider
v-model="formData.t1_weight"
:min="0"
:max="100"
:step="0.01"
show-input
:disabled="!isLotteryConfigEmpty()"
/>
</el-form-item> </el-form-item>
<el-form-item v-if="!formData.lottery_config_id" label="T3池权重(%)" prop="t3_weight"> <el-form-item label="T2池权重(%)" prop="t2_weight">
<el-slider v-model="formData.t3_weight" :min="0" :max="100" :step="0.01" show-input /> <el-slider
v-model="formData.t2_weight"
:min="0"
:max="100"
:step="0.01"
show-input
:disabled="!isLotteryConfigEmpty()"
/>
</el-form-item> </el-form-item>
<el-form-item v-if="!formData.lottery_config_id" label="T4池权重(%)" prop="t4_weight"> <el-form-item label="T3池权重(%)" prop="t3_weight">
<el-slider v-model="formData.t4_weight" :min="0" :max="100" :step="0.01" show-input /> <el-slider
v-model="formData.t3_weight"
:min="0"
:max="100"
:step="0.01"
show-input
:disabled="!isLotteryConfigEmpty()"
/>
</el-form-item> </el-form-item>
<el-form-item v-if="!formData.lottery_config_id" label="T5池权重(%)" prop="t5_weight"> <el-form-item label="T4池权重(%)" prop="t4_weight">
<el-slider v-model="formData.t5_weight" :min="0" :max="100" :step="0.01" show-input /> <el-slider
v-model="formData.t4_weight"
:min="0"
:max="100"
:step="0.01"
show-input
:disabled="!isLotteryConfigEmpty()"
/>
</el-form-item> </el-form-item>
<el-form-item v-if="!formData.lottery_config_id"> <el-form-item label="T5池权重(%)" prop="t5_weight">
<el-slider
v-model="formData.t5_weight"
:min="0"
:max="100"
:step="0.01"
show-input
:disabled="!isLotteryConfigEmpty()"
/>
</el-form-item>
<el-form-item v-if="isLotteryConfigEmpty()">
<div class="text-gray-500 text-sm"> <div class="text-gray-500 text-sm">
五个池权重总和<span :class="Math.abs(weightsSum - 100) > 0.01 ? 'text-red-500' : ''">{{ 五个池权重总和<span :class="Math.abs(weightsSum - 100) > 0.01 ? 'text-red-500' : ''">{{
weightsSum weightsSum
@@ -130,6 +189,18 @@
return WEIGHT_FIELDS.reduce((sum, key) => sum + Number(formData[key] ?? 0), 0) return WEIGHT_FIELDS.reduce((sum, key) => sum + Number(formData[key] ?? 0), 0)
}) })
/** 当前彩金池配置的 T1T5 权重展示文案 */
const currentLotteryConfigWeightsText = computed(() => {
const c = currentLotteryConfig.value
if (!c) return '-'
const t1 = c.t1_weight ?? 0
const t2 = c.t2_weight ?? 0
const t3 = c.t3_weight ?? 0
const t4 = c.t4_weight ?? 0
const t5 = c.t5_weight ?? 0
return `${t1}% / ${t2}% / ${t3}% / ${t4}% / ${t5}%`
})
/** 新增时密码必填,编辑时选填 */ /** 新增时密码必填,编辑时选填 */
const passwordRules = computed(() => const passwordRules = computed(() =>
props.dialogType === 'add' ? [{ required: true, message: '密码必需填写', trigger: 'blur' }] : [] props.dialogType === 'add' ? [{ required: true, message: '密码必需填写', trigger: 'blur' }] : []
@@ -151,6 +222,7 @@
password: '', password: '',
status: 1 as number, status: 1 as number,
coin: 0 as number, coin: 0 as number,
/** 彩金池配置 ID空 = 自定义权重,否则 = DiceLotteryConfig.id */
lottery_config_id: null as number | null, lottery_config_id: null as number | null,
t1_weight: 0 as number, t1_weight: 0 as number,
t2_weight: 0 as number, t2_weight: 0 as number,
@@ -161,8 +233,48 @@
const formData = reactive({ ...initialFormData }) const formData = reactive({ ...initialFormData })
/** 彩金池配置下拉选项 */ /** 彩金池配置下拉选项DiceLotteryConfig id、name */
const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([]) const lotteryConfigOptions = ref<Array<{ id: number; name: string }>>([])
/** 彩金池选项加载中 */
const lotteryConfigLoading = ref(false)
/** 当前选中的 DiceLotteryConfig 完整数据(用于展示) */
const currentLotteryConfig = ref<Record<string, any> | null>(null)
function lotteryConfigTypeText(type: unknown): string {
const t = Number(type)
if (t === 0) return '付费'
if (t === 1) return '赠送'
return t ? `类型${t}` : '-'
}
/** 是否为空/自定义权重(未选彩金池或选 0 */
function isLotteryConfigEmpty(): boolean {
const v = formData.lottery_config_id
return v == null || v === 0
}
/** 根据当前 lottery_config_id 加载 DiceLotteryConfig并将五个权重写入当前 player.*_weight */
async function loadCurrentLotteryConfig() {
const id = formData.lottery_config_id
if (id == null || id === 0) {
currentLotteryConfig.value = null
return
}
try {
const res = await lotteryConfigApi.read(id)
const row = (res as any)?.data ?? (res as any)
if (row && typeof row === 'object') {
currentLotteryConfig.value = row
WEIGHT_FIELDS.forEach((key) => {
;(formData as any)[key] = Number(row[key] ?? 0)
})
} else {
currentLotteryConfig.value = null
}
} catch {
currentLotteryConfig.value = null
}
}
watch( watch(
() => props.modelValue, () => props.modelValue,
@@ -171,37 +283,51 @@
} }
) )
/** 选择彩金池配置时,拉取该配置的权重填入表单(仅展示/备份lottery_config_id 非空时后端以配置为准) */ /** 选择彩金池,拉取该配置的五个权重并写入当前 player.*_weight并更新当前配置展示 */
async function onLotteryConfigChange(lotteryConfigId: number | null) { async function onLotteryConfigChange(lotteryConfigId: number | null | undefined) {
if (!lotteryConfigId) return if (lotteryConfigId == null || lotteryConfigId === 0) {
currentLotteryConfig.value = null
return
}
try { try {
const res = await lotteryConfigApi.read(lotteryConfigId) const res = await lotteryConfigApi.read(lotteryConfigId)
const row = (res as any)?.data const row = (res as any)?.data ?? (res as any)
if (row) { if (row && typeof row === 'object') {
WEIGHT_FIELDS.forEach((key) => { WEIGHT_FIELDS.forEach((key) => {
;(formData as any)[key] = Number(row[key] ?? 0) ;(formData as any)[key] = Number(row[key] ?? 0)
}) })
currentLotteryConfig.value = row
} else {
currentLotteryConfig.value = null
} }
} catch (err) { } catch (err) {
console.warn('拉取彩金池配置权重失败', err) console.warn('拉取彩金池配置失败', err)
currentLotteryConfig.value = null
} }
} }
const initPage = async () => { const initPage = async () => {
currentLotteryConfig.value = null
Object.assign(formData, initialFormData) Object.assign(formData, initialFormData)
await loadLotteryConfigOptions() await loadLotteryConfigOptions()
if (props.data) { if (props.data) {
await nextTick() await nextTick()
initForm() initForm()
if (!isLotteryConfigEmpty()) {
await loadCurrentLotteryConfig()
}
} }
} }
/** 从 DiceLotteryConfig 拉取彩金池配置下拉选项 */ /** 从玩家控制器获取 DiceLotteryConfig id/name 列表,供 lottery_config_id 下拉使用 */
async function loadLotteryConfigOptions() { async function loadLotteryConfigOptions() {
lotteryConfigLoading.value = true
try { try {
lotteryConfigOptions.value = await lotteryConfigApi.getOptions() lotteryConfigOptions.value = await api.getLotteryConfigOptions()
} catch { } catch {
lotteryConfigOptions.value = [] lotteryConfigOptions.value = []
} finally {
lotteryConfigLoading.value = false
} }
} }
@@ -227,8 +353,14 @@
} }
const val = props.data[key] const val = props.data[key]
if (numKeys.includes(key)) { if (numKeys.includes(key)) {
;(formData as any)[key] = if (key === 'id') {
key === 'id' ? (val != null ? Number(val) || null : null) : Number(val) || 0 ;(formData as any)[key] = val != null ? Number(val) || null : null
} else if (key === 'lottery_config_id') {
const num = Number(val)
;(formData as any)[key] = val != null && !Number.isNaN(num) && num !== 0 ? num : null
} else {
;(formData as any)[key] = Number(val) || 0
}
} else { } else {
;(formData as any)[key] = val ?? '' ;(formData as any)[key] = val ?? ''
} }
@@ -244,12 +376,15 @@
if (!formRef.value) return if (!formRef.value) return
try { try {
await formRef.value.validate() await formRef.value.validate()
const useCustomWeights = !formData.lottery_config_id const useCustomWeights = isLotteryConfigEmpty()
if (useCustomWeights && Math.abs(weightsSum.value - 100) > 0.01) { if (useCustomWeights && Math.abs(weightsSum.value - 100) > 0.01) {
ElMessage.warning('五个池权重总和必须为100%') ElMessage.warning('五个池权重总和必须为100%')
return return
} }
const payload = { ...formData } const payload = { ...formData }
if (isLotteryConfigEmpty()) {
;(payload as any).lottery_config_id = null
}
if (props.dialogType === 'edit' && !payload.password) { if (props.dialogType === 'edit' && !payload.password) {
delete (payload as any).password delete (payload as any).password
} }
@@ -267,3 +402,31 @@
} }
} }
</script> </script>
<style lang="scss" scoped>
.current-config-block {
margin-bottom: 12px;
}
.current-lottery-config {
padding: 10px 12px;
background: var(--el-fill-color-light);
border-radius: 6px;
font-size: 13px;
color: var(--el-text-color-regular);
.config-row {
margin-bottom: 6px;
line-height: 1.5;
&:last-child {
margin-bottom: 0;
}
}
.config-label {
color: var(--el-text-color-secondary);
margin-right: 4px;
}
}
</style>

View File

@@ -64,7 +64,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import lotteryConfigApi from '../../../api/lottery_config/index' import api from '../../../api/player/index'
interface Props { interface Props {
modelValue: Record<string, any> modelValue: Record<string, any>
@@ -79,10 +79,10 @@
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 拉取彩金池配置下拉选项,用于 lottery_config_id 筛选 */ /** 从玩家控制器获取 DiceLotteryConfig id/name 列表,用于 lottery_config_id 筛选 */
onMounted(async () => { onMounted(async () => {
try { try {
lotteryConfigOptions.value = await lotteryConfigApi.getOptions() lotteryConfigOptions.value = await api.getLotteryConfigOptions()
} catch { } catch {
lotteryConfigOptions.value = [] lotteryConfigOptions.value = []
} }

View File

@@ -122,6 +122,7 @@
{ 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: '权重(%)', width: 100, align: 'center' },
// { prop: 'create_time', label: '创建时间', sortable: true, align: 'center' }, // { prop: 'create_time', label: '创建时间', sortable: true, align: 'center' },
{ {
prop: 'operation', prop: 'operation',

View File

@@ -34,8 +34,12 @@
<el-option label="T3" value="T3" /> <el-option label="T3" value="T3" />
<el-option label="T4" value="T4" /> <el-option label="T4" value="T4" />
<el-option label="T5" value="T5" /> <el-option label="T5" value="T5" />
<el-option label="BIGWIN超级大奖" value="BIGWIN" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="formData.tier === 'BIGWIN'" label="权重(%)" prop="weight">
<el-slider v-model="formData.weight" :min="0" :max="100" :step="0.01" show-input />
</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"
@@ -95,7 +99,24 @@
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: [
{
validator: (_rule: unknown, value: number | null, callback: (e?: Error) => void) => {
if (formData.tier !== 'BIGWIN') {
callback()
return
}
const n = value != null ? Number(value) : NaN
if (Number.isNaN(n) || n < 0 || n > 100) {
callback(new Error('权重仅 BIGWIN 可设定,且必须为 0-100%'))
return
}
callback()
},
trigger: 'blur'
}
]
}) })
/** /**
@@ -107,6 +128,7 @@
ui_text: '', ui_text: '',
real_ev: '', real_ev: '',
tier: '', tier: '',
weight: 0 as number,
remark: '' remark: ''
} }
@@ -141,14 +163,21 @@
} }
/** /**
* 初始化表单数据 * 初始化表单数据(数值字段转为 number便于滑块/输入框正确回显)
*/ */
const initForm = () => { const initForm = () => {
if (props.data) { if (!props.data) return
for (const key in formData) { const numKeys = ['id', 'grid_number', 'real_ev', 'weight']
if (props.data[key] != null && props.data[key] != undefined) { for (const key of Object.keys(formData)) {
;(formData as any)[key] = props.data[key] if (!(key in props.data)) continue
} const val = props.data[key]
if (val == null || val === undefined) continue
if (numKeys.includes(key)) {
const numVal = Number(val)
;(formData as Record<string, unknown>)[key] =
key === 'id' ? numVal || null : Number.isNaN(numVal) ? 0 : numVal
} else {
;(formData as Record<string, unknown>)[key] = val ?? ''
} }
} }
} }
@@ -168,11 +197,18 @@
if (!formRef.value) return if (!formRef.value) return
try { try {
await formRef.value.validate() await formRef.value.validate()
const payload = { ...formData }
if (payload.tier !== 'BIGWIN') {
payload.weight = 0
} else {
const w = Number(payload.weight)
payload.weight = Number.isNaN(w) ? 0 : Math.max(0, Math.min(100, w))
}
if (props.dialogType === 'add') { if (props.dialogType === 'add') {
await api.save(formData) await api.save(payload)
ElMessage.success('新增成功') ElMessage.success('新增成功')
} else { } else {
await api.update(formData) await api.update(payload)
ElMessage.success('修改成功') ElMessage.success('修改成功')
} }
emit('success') emit('success')

View File

@@ -150,6 +150,7 @@ class GameController extends OpenController
'start_index' => 0, 'start_index' => 0,
'target_index' => 0, 'target_index' => 0,
'roll_array' => '[]', 'roll_array' => '[]',
'roll_number' => 0,
'status' => PlayStartLogic::RECORD_STATUS_TIMEOUT, 'status' => PlayStartLogic::RECORD_STATUS_TIMEOUT,
]); ]);
} catch (\Exception $inner) { } catch (\Exception $inner) {

View File

@@ -34,8 +34,10 @@ class PlayStartLogic
/** 开启对局最低余额 = |DiceRewardConfig 最小 real_ev + 100| */ /** 开启对局最低余额 = |DiceRewardConfig 最小 real_ev + 100| */
private const MIN_COIN_EXTRA = 100; private const MIN_COIN_EXTRA = 100;
/** 豹子号中大奖额外平台币(可从 dice_config 等配置读取 */ /** 豹子号中大奖额外平台币(无 BIGWIN 配置时兜底 */
private const SUPER_WIN_BONUS = 500; private const SUPER_WIN_BONUS = 500;
/** 可触发超级大奖的 grid_number5=全1 10=全2 15=全3 20=全4 25=全5 */
private const SUPER_WIN_GRID_NUMBERS = [5, 10, 15, 20, 25];
/** /**
* 执行一局游戏 * 执行一局游戏
@@ -112,7 +114,30 @@ class PlayStartLogic
? (int) ($startRecord['s_end_index'] ?? 0) ? (int) ($startRecord['s_end_index'] ?? 0)
: (int) ($startRecord['n_end_index'] ?? 0); : (int) ($startRecord['n_end_index'] ?? 0);
$rollNumber = (int) ($startRecord['grid_number'] ?? 0); $rollNumber = (int) ($startRecord['grid_number'] ?? 0);
$rollArray = $this->generateRollArrayFromSum($rollNumber); $realEv = (float) ($chosen['real_ev'] ?? 0);
$rewardWinCoin = 100 + $realEv; // 摇色子中奖平台币 = 100 + DiceRewardConfig.real_ev
// 当抽到的 grid_number 为 5/10/15/20/25 时,从缓存查 tier=BIGWIN 同 grid_number 的配置,按 weight 决定是否生成豹子组合
$superWinCoin = 0;
$isWin = 0;
if (in_array($rollNumber, self::SUPER_WIN_GRID_NUMBERS, true)) {
$bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber);
$weight = $bigWinConfig !== null
? max(0.0, min(100.0, (float) ($bigWinConfig['weight'] ?? 0)))
: 100.0;
$roll = mt_rand(1, 10000) / 10000;
if ($roll <= $weight / 100) {
$rollArray = $this->getSuperWinRollArray($rollNumber);
$isWin = 1;
$superWinCoin = $bigWinConfig !== null
? 100 + (float) ($bigWinConfig['real_ev'] ?? 0)
: self::SUPER_WIN_BONUS;
} else {
$rollArray = $this->generateNonSuperWinRollArrayWithSum($rollNumber);
}
} else {
$rollArray = $this->generateRollArrayFromSum($rollNumber);
}
Log::info(sprintf( Log::info(sprintf(
'摇取点数 roll_number=%d, 方向=%d, start_index=%d, target_index=%d', '摇取点数 roll_number=%d, 方向=%d, start_index=%d, target_index=%d',
@@ -121,17 +146,6 @@ class PlayStartLogic
$startIndex, $startIndex,
$targetIndex $targetIndex
)); ));
$realEv = (float) ($chosen['real_ev'] ?? 0);
$rewardWinCoin = 100 + $realEv; // 摇色子中奖平台币 = 100 + DiceRewardConfig.real_ev
$isSuperWin = DicePlayRecord::isSuperWin($rollArray);
// 豹子中大奖时从缓存查 tier=BIGWIN 且 grid_number=roll_number 的奖励配置,取 real_ev 计算中大奖平台币
$superWinCoin = 0;
if ($isSuperWin) {
$bigWinConfig = DiceRewardConfig::getCachedByTierAndGridNumber('BIGWIN', $rollNumber);
$superWinCoin = $bigWinConfig !== null
? 100 + (float) ($bigWinConfig['real_ev'] ?? 0)
: self::SUPER_WIN_BONUS;
}
$winCoin = $superWinCoin + $rewardWinCoin; // 赢取平台币 = 中大奖 + 摇色子中奖 $winCoin = $superWinCoin + $rewardWinCoin; // 赢取平台币 = 中大奖 + 摇色子中奖
$record = null; $record = null;
@@ -139,7 +153,6 @@ class PlayStartLogic
$rewardId = $chosenId; $rewardId = $chosenId;
$configName = (string) ($config->name ?? ''); $configName = (string) ($config->name ?? '');
$isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5'; $isTierT5 = (string) ($chosen['tier'] ?? '') === 'T5';
$isWin = $isSuperWin ? 1 : 0;
try { try {
Db::transaction(function () use ( Db::transaction(function () use (
$playerId, $playerId,
@@ -173,6 +186,7 @@ class PlayStartLogic
'start_index' => $startIndex, 'start_index' => $startIndex,
'target_index' => $targetIndex, 'target_index' => $targetIndex,
'roll_array' => is_array($rollArray) ? json_encode($rollArray) : $rollArray, 'roll_array' => is_array($rollArray) ? json_encode($rollArray) : $rollArray,
'roll_number' => is_array($rollArray) ? array_sum($rollArray) : 0,
'lottery_name' => $configName, 'lottery_name' => $configName,
'status' => self::RECORD_STATUS_SUCCESS, 'status' => self::RECORD_STATUS_SUCCESS,
]); ]);
@@ -239,6 +253,7 @@ class PlayStartLogic
'start_index' => $startIndex, 'start_index' => $startIndex,
'target_index' => 0, 'target_index' => 0,
'roll_array' => '[]', 'roll_array' => '[]',
'roll_number' => 0,
'status' => self::RECORD_STATUS_TIMEOUT, 'status' => self::RECORD_STATUS_TIMEOUT,
]); ]);
} catch (\Throwable $_) { } catch (\Throwable $_) {
@@ -290,4 +305,51 @@ class PlayStartLogic
shuffle($arr); shuffle($arr);
return array_values($arr); return array_values($arr);
} }
/**
* 豹子组合grid_number 5->[1,1,1,1,1]10->[2,2,2,2,2]15->[3,3,3,3,3]20->[4,4,4,4,4]25->[5,5,5,5,5]
* @return int[]
*/
private function getSuperWinRollArray(int $gridNumber): array
{
$n = (int) ($gridNumber / 5);
$n = max(1, min(5, $n));
return array_fill(0, 5, $n);
}
/**
* 生成总和为 $sum 且非豹子的 5 个色子1-6sum=5 时仅 [1,1,1,1,1] 可能,仍返回该组合
* @return int[]
*/
private function generateNonSuperWinRollArrayWithSum(int $sum): array
{
$sum = max(5, min(30, $sum));
$super = $this->getSuperWinRollArray($sum);
if ($sum === 5) {
return $super;
}
$arr = $super;
$maxAttempts = 20;
for ($a = 0; $a < $maxAttempts; $a++) {
$idx = array_rand($arr);
$j = array_rand($arr);
if ($idx === $j) {
$j = ($j + 1) % 5;
}
$i = $idx;
if ($arr[$i] >= 2 && $arr[$j] <= 5) {
$arr[$i]--;
$arr[$j]++;
shuffle($arr);
return array_values($arr);
}
if ($arr[$i] <= 5 && $arr[$j] >= 2) {
$arr[$i]++;
$arr[$j]--;
shuffle($arr);
return array_values($arr);
}
}
return $this->generateRollArrayFromSum($sum);
}
} }

View File

@@ -55,6 +55,9 @@ class UserLogic
$player = DicePlayer::where('username', $username)->find(); $player = DicePlayer::where('username', $username)->find();
if ($player) { if ($player) {
if ((int) ($player->status ?? 1) === 0) {
throw new ApiException('账号已被禁用,无法登录');
}
$hashed = $this->hashPassword($password); $hashed = $this->hashPassword($password);
if ($player->password !== $hashed) { if ($player->password !== $hashed) {
throw new ApiException('密码错误'); throw new ApiException('密码错误');

View File

@@ -6,6 +6,7 @@
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
namespace app\dice\controller\lottery_config; namespace app\dice\controller\lottery_config;
use app\dice\model\lottery_config\DiceLotteryConfig;
use plugin\saiadmin\basic\BaseController; use plugin\saiadmin\basic\BaseController;
use app\dice\logic\lottery_config\DiceLotteryConfigLogic; use app\dice\logic\lottery_config\DiceLotteryConfigLogic;
use app\dice\validate\lottery_config\DiceLotteryConfigValidate; use app\dice\validate\lottery_config\DiceLotteryConfigValidate;
@@ -28,6 +29,21 @@ class DiceLotteryConfigController extends BaseController
parent::__construct(); parent::__construct();
} }
/**
* 获取 DiceLotteryConfig 列表数据,仅含 id、name用于 lottery_config_id 下拉(值为 id显示为 name
* @param Request $request
* @return Response 返回 [ ['id' => int, 'name' => string], ... ]
*/
#[Permission('色子奖池配置列表', 'dice:lottery_config:index:index')]
public function getOptions(Request $request): Response
{
$list = DiceLotteryConfig::field('id,name')->order('id', 'asc')->select();
$data = $list->map(function ($item) {
return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')];
})->toArray();
return $this->success($data);
}
/** /**
* 数据列表 * 数据列表
* @param Request $request * @param Request $request

View File

@@ -46,6 +46,8 @@ class DicePlayRecordController extends BaseController
['is_win', ''], ['is_win', ''],
['win_coin_min', ''], ['win_coin_min', ''],
['win_coin_max', ''], ['win_coin_max', ''],
['roll_number_min', ''],
['roll_number_max', ''],
['reward_ui_text', ''], ['reward_ui_text', ''],
['reward_tier', ''], ['reward_tier', ''],
['direction', ''], ['direction', ''],

View File

@@ -6,6 +6,7 @@
// +---------------------------------------------------------------------- // +----------------------------------------------------------------------
namespace app\dice\controller\player; namespace app\dice\controller\player;
use app\dice\model\lottery_config\DiceLotteryConfig;
use plugin\saiadmin\basic\BaseController; use plugin\saiadmin\basic\BaseController;
use app\dice\logic\player\DicePlayerLogic; use app\dice\logic\player\DicePlayerLogic;
use app\dice\validate\player\DicePlayerValidate; use app\dice\validate\player\DicePlayerValidate;
@@ -28,6 +29,21 @@ class DicePlayerController extends BaseController
parent::__construct(); parent::__construct();
} }
/**
* 获取彩金池配置选项DiceLotteryConfig.id、name供前端 lottery_config_id 下拉使用
* @param Request $request
* @return Response 返回 [ ['id' => int, 'name' => string], ... ]
*/
#[Permission('大富翁-玩家列表', 'dice:player:index:index')]
public function getLotteryConfigOptions(Request $request): Response
{
$list = DiceLotteryConfig::field('id,name')->order('id', 'asc')->select();
$data = $list->map(function ($item) {
return ['id' => (int) $item['id'], 'name' => (string) ($item['name'] ?? '')];
})->toArray();
return $this->success($data);
}
/** /**
* 数据列表 * 数据列表
* @param Request $request * @param Request $request

View File

@@ -43,16 +43,18 @@ class DicePlayRecordLogic extends BaseLogic
} }
/** /**
* 将 roll_array 从数组转为 JSON 字符串 * 将 roll_array 转为 JSON 字符串,并确保 roll_number 与摇取点数一致
*/ */
private function normalizeRollArray(array $data): array private function normalizeRollArray(array $data): array
{ {
if (!array_key_exists('roll_array', $data)) { if (array_key_exists('roll_array', $data)) {
return $data; $val = $data['roll_array'];
} if (is_array($val)) {
$val = $data['roll_array']; $data['roll_array'] = json_encode($val, JSON_UNESCAPED_UNICODE);
if (is_array($val)) { if (!isset($data['roll_number'])) {
$data['roll_array'] = json_encode($val, JSON_UNESCAPED_UNICODE); $data['roll_number'] = array_sum($val);
}
}
} }
return $data; return $data;
} }

View File

@@ -13,6 +13,7 @@ use app\dice\model\reward_config\DiceRewardConfig;
/** /**
* 奖励配置逻辑层 * 奖励配置逻辑层
* weight 仅 tier=BIGWIN 时可设定,保存时非 BIGWIN 强制 weight=0
*/ */
class DiceRewardConfigLogic extends BaseLogic class DiceRewardConfigLogic extends BaseLogic
{ {
@@ -24,4 +25,36 @@ class DiceRewardConfigLogic extends BaseLogic
$this->model = new DiceRewardConfig(); $this->model = new DiceRewardConfig();
} }
/**
* 新增前:非 BIGWIN 时强制 weight=0
*/
public function add(array $data): mixed
{
$data = $this->normalizeWeightByTier($data);
return parent::add($data);
}
/**
* 修改前:非 BIGWIN 时强制 weight=0
*/
public function edit($id, array $data): mixed
{
$data = $this->normalizeWeightByTier($data);
return parent::edit($id, $data);
}
/**
* 仅 tier=BIGWIN 时保留 weight且限制 0-100否则强制为 0
*/
private function normalizeWeightByTier(array $data): array
{
$tier = isset($data['tier']) ? (string) $data['tier'] : '';
if ($tier !== 'BIGWIN') {
$data['weight'] = 0;
return $data;
}
$w = isset($data['weight']) ? (float) $data['weight'] : 0;
$data['weight'] = max(0, min(100, $w));
return $data;
}
} }

View File

@@ -31,6 +31,7 @@ use think\model\relation\BelongsTo;
* @property $start_index 起始索引 * @property $start_index 起始索引
* @property $target_index 结束索引 * @property $target_index 结束索引
* @property $roll_array 摇取点数,格式:[1,2,3,4,5]5个点数 * @property $roll_array 摇取点数,格式:[1,2,3,4,5]5个点数
* @property $roll_number 摇取点数和5个色子点数之和5-30
* @property $lottery_name 奖池名 * @property $lottery_name 奖池名
* @property $status 状态:0=超时/失败 1=成功 * @property $status 状态:0=超时/失败 1=成功
* @property $create_time 创建时间 * @property $create_time 创建时间
@@ -222,4 +223,20 @@ class DicePlayRecord extends BaseModel
$query->where('direction', '=', $value); $query->where('direction', '=', $value);
} }
} }
/** 摇取点数和下限 */
public function searchRollNumberMinAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('roll_number', '>=', $value);
}
}
/** 摇取点数和上限 */
public function searchRollNumberMaxAttr($query, $value)
{
if ($value !== '' && $value !== null) {
$query->where('roll_number', '<=', $value);
}
}
} }

View File

@@ -77,6 +77,16 @@ 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
try {
$lotteryConfigId = $model->getAttr('lottery_config_id');
} catch (\Throwable $e) {
$lotteryConfigId = null;
}
if ($lotteryConfigId === null || $lotteryConfigId === '' || (int) $lotteryConfigId === 0) {
$config = DiceLotteryConfig::where('type', 0)->find();
$model->setAttr('lottery_config_id', $config ? (int) $config->id : 0);
}
// 彩金池权重默认取 type=0 的奖池配置 // 彩金池权重默认取 type=0 的奖池配置
self::setDefaultWeightsFromLotteryConfig($model); self::setDefaultWeightsFromLotteryConfig($model);
} }

View File

@@ -20,6 +20,7 @@ use support\think\Cache;
* @property $ui_text 前端显示文本 * @property $ui_text 前端显示文本
* @property $real_ev 真实资金结算 * @property $real_ev 真实资金结算
* @property $tier 所属档位 * @property $tier 所属档位
* @property $weight 权重%(仅 tier=BIGWIN 时可设定0-100
* @property $s_end_index 顺时针结束索引 * @property $s_end_index 顺时针结束索引
* @property $n_end_index 逆时针结束索引 * @property $n_end_index 逆时针结束索引
* @property $remark 备注 * @property $remark 备注
@@ -80,7 +81,8 @@ class DiceRewardConfig extends BaseModel
} }
/** /**
* 重新从数据库加载并写入缓存(保存时调用),构建列表与索引 * 重新从数据库加载并写入缓存(DiceRewardConfig 新增/修改/删除后调用),构建列表与索引
* 实例化结果含完整行(含 weight供 playStart 从缓存中查找 BIGWIN 的 weight 按概率抽奖
*/ */
public static function refreshCache(): void public static function refreshCache(): void
{ {
@@ -148,10 +150,11 @@ class DiceRewardConfig extends BaseModel
} }
/** /**
* 从缓存按档位 + 色子点数取一条奖励配置(用于超级大奖 tier=BIGWIN + grid_number=roll_number * 从缓存实例按档位 + 色子点数取一条奖励配置(用于超级大奖 tier=BIGWIN + grid_number=roll_number
* 返回行含 weight0-100playStart 据此概率抽奖weight=100 表示摇到该 roll_number 时 100% 中超级大奖
* @param string $tier 档位,如 BIGWIN * @param string $tier 档位,如 BIGWIN
* @param int $gridNumber 色子点数(摇出总和) * @param int $gridNumber 色子点数(摇出总和 roll_number
* @return array|null 配置行或 null * @return array|null 配置行(含 weight、real_ev 等)或 null
*/ */
public static function getCachedByTierAndGridNumber(string $tier, int $gridNumber): ?array public static function getCachedByTierAndGridNumber(string $tier, int $gridNumber): ?array
{ {
@@ -277,4 +280,20 @@ class DiceRewardConfig extends BaseModel
$query->where('tier', '=', $value); $query->where('tier', '=', $value);
} }
} }
/** 权重下限(仅 tier=BIGWIN 时有意义) */
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);
}
}
} }

View File

@@ -10,45 +10,56 @@ use plugin\saiadmin\basic\BaseValidate;
/** /**
* 奖励配置验证器 * 奖励配置验证器
* weight 仅当 tier=BIGWIN 时可设定,且严格限制 0-100%
*/ */
class DiceRewardConfigValidate extends BaseValidate class DiceRewardConfigValidate extends BaseValidate
{ {
/** /**
* 定义验证规则 * 定义验证规则
*/ */
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',
]; ];
/** /**
* 定义错误信息 * 定义错误信息
*/ */
protected $message = [ protected $message = [
'grid_number' => '色子点数必须填写', 'grid_number' => '色子点数必须填写',
'ui_text' => '前端显示文本必须填写', 'ui_text' => '前端显示文本必须填写',
'real_ev' => '真实资金结算必须填写', 'real_ev' => '真实资金结算必须填写',
'tier' => '所属档位必须填写', 'tier' => '所属档位必须填写',
'weight' => '权重仅 tier=BIGWIN 时可设定,且必须为 0-100',
]; ];
/** /**
* 定义场景 * 定义场景
*/ */
protected $scene = [ protected $scene = [
'save' => [ 'save' => ['grid_number', 'ui_text', 'real_ev', 'tier', 'weight'],
'grid_number', 'update' => ['grid_number', 'ui_text', 'real_ev', 'tier', 'weight'],
'ui_text',
'real_ev',
'tier',
],
'update' => [
'grid_number',
'ui_text',
'real_ev',
'tier',
],
]; ];
/**
* weight仅 tier=BIGWIN 时可设定,严格限制 0-100%
*/
protected function checkWeight($value, $rule = '', $data = []): bool
{
$tier = isset($data['tier']) ? (string) $data['tier'] : '';
if ($tier !== 'BIGWIN') {
return true;
}
$num = is_numeric($value) ? (float) $value : null;
if ($num === null) {
return false;
}
if ($num < 0 || $num > 100) {
return false;
}
return true;
}
} }