1.优化游戏记录审核
2.优化游戏配置页面 3.备份数据库
This commit is contained in:
@@ -8,6 +8,7 @@ use app\common\library\game\DepositTier;
|
||||
use app\common\library\game\FinanceCashierConfig;
|
||||
use app\common\library\game\StreakWinReward;
|
||||
use app\common\library\game\ZiHuaDictionary as ZiHuaDictionaryLib;
|
||||
use support\think\Db;
|
||||
use support\Response;
|
||||
use Webman\Http\Request as WebmanRequest;
|
||||
|
||||
@@ -50,6 +51,17 @@ class GameConfig extends Backend
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
protected function formExcludedKeys(): array
|
||||
{
|
||||
return [
|
||||
'period_auto_create_enabled',
|
||||
'period_manual_create_enabled',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表:排除独立表单维护的配置键
|
||||
*/
|
||||
@@ -128,4 +140,84 @@ class GameConfig extends Backend
|
||||
'total' => $res->total(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单批量保存:一次提交当前页面全部配置
|
||||
*/
|
||||
public function save(WebmanRequest $request): Response
|
||||
{
|
||||
$response = $this->initializeBackend($request);
|
||||
if ($response !== null) {
|
||||
return $response;
|
||||
}
|
||||
if ($request->method() !== 'POST') {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$payload = $request->post();
|
||||
if (!is_array($payload)) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$items = $payload['items'] ?? null;
|
||||
if (!is_array($items)) {
|
||||
return $this->error(__('Parameter error'));
|
||||
}
|
||||
|
||||
$exclude = array_merge($this->excludedConfigKeys(), $this->formExcludedKeys());
|
||||
$rows = Db::name('game_config')
|
||||
->whereNotIn('config_key', $exclude)
|
||||
->field(['id', 'config_key', 'value_type'])
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
if ($rows === []) {
|
||||
return $this->success(__('Operation completed'));
|
||||
}
|
||||
|
||||
$allowMap = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!isset($row['config_key']) || !is_string($row['config_key']) || $row['config_key'] === '') {
|
||||
continue;
|
||||
}
|
||||
$allowMap[$row['config_key']] = $row;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
Db::startTrans();
|
||||
try {
|
||||
foreach ($items as $configKey => $configValue) {
|
||||
if (!is_string($configKey) || $configKey === '' || !isset($allowMap[$configKey])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$valueType = isset($allowMap[$configKey]['value_type']) && is_string($allowMap[$configKey]['value_type'])
|
||||
? $allowMap[$configKey]['value_type']
|
||||
: 'string';
|
||||
|
||||
$persistValue = $configValue;
|
||||
if (is_array($persistValue)) {
|
||||
if ($valueType === 'json') {
|
||||
$persistValue = json_encode($persistValue, JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
$persistValue = '';
|
||||
}
|
||||
}
|
||||
if ($persistValue === null) {
|
||||
$persistValue = '';
|
||||
}
|
||||
|
||||
Db::name('game_config')->where('id', $allowMap[$configKey]['id'])->update([
|
||||
'config_value' => (string) $persistValue,
|
||||
'update_time' => $now,
|
||||
]);
|
||||
}
|
||||
Db::commit();
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
|
||||
return $this->success(__('Operation completed'));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,25 @@
|
||||
export default {
|
||||
'quick Search Fields': 'ID / Key / Remark',
|
||||
'form tab label': 'Game config',
|
||||
'save success': 'Saved successfully',
|
||||
'field period_auto_create_enabled': 'Auto create next period',
|
||||
'field_tip period_auto_create_enabled': 'Whether to automatically create the next period',
|
||||
'field period_manual_create_enabled': 'Manual create next period',
|
||||
'field_tip period_manual_create_enabled': 'Whether manual creation of the next period is allowed',
|
||||
'field period_seconds': 'Period duration (seconds)',
|
||||
'field_tip period_seconds': 'Duration of each period in seconds',
|
||||
'field bet_seconds': 'Betting duration (seconds)',
|
||||
'field_tip bet_seconds': 'How many seconds betting stays open in each period',
|
||||
'field pick_max_number_count': 'Max numbers per ticket',
|
||||
'field_tip pick_max_number_count': 'Maximum amount of selectable numbers per ticket',
|
||||
'field min_bet_per_number': 'Min bet per number',
|
||||
'field_tip min_bet_per_number': 'Minimum bet amount per selected number',
|
||||
'field max_bet_per_number': 'Max bet per number',
|
||||
'field_tip max_bet_per_number': 'Maximum bet amount per selected number',
|
||||
'field withdraw_bet_flow_ratio': 'Withdraw flow ratio',
|
||||
'field_tip withdraw_bet_flow_ratio': 'Required betting flow ratio before withdrawal',
|
||||
'field jackpot_max_amount': 'Jackpot review threshold',
|
||||
'field_tip jackpot_max_amount': 'Winning amount threshold that requires manual jackpot review',
|
||||
id: 'ID',
|
||||
config_key: 'Config key',
|
||||
config_value: 'Value',
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
'status 1': 'Pending draw',
|
||||
'status 2': 'Settled',
|
||||
'status 3': 'Refunded',
|
||||
'status 4': 'Returned',
|
||||
'status 4': 'Returned (review rejected)',
|
||||
'status 5': 'Pending review',
|
||||
review_title: 'Win review',
|
||||
review_open: 'Review',
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
export default {
|
||||
'quick Search Fields': 'ID/参数键/说明',
|
||||
'form tab label': '游戏参数配置',
|
||||
'save success': '保存成功',
|
||||
'field period_auto_create_enabled': '自动创建下一期开关',
|
||||
'field_tip period_auto_create_enabled': '是否允许自动创建下一期',
|
||||
'field period_manual_create_enabled': '手动创建下一期开关',
|
||||
'field_tip period_manual_create_enabled': '是否允许手动创建下一期',
|
||||
'field period_seconds': '每期时长(秒)',
|
||||
'field_tip period_seconds': '每一局的总时长(秒)',
|
||||
'field bet_seconds': '下注时长(秒)',
|
||||
'field_tip bet_seconds': '每一局允许下注的时长(秒)',
|
||||
'field pick_max_number_count': '单注最多号码个数',
|
||||
'field_tip pick_max_number_count': '单注最多可选号码数量',
|
||||
'field min_bet_per_number': '单号最小下注额',
|
||||
'field_tip min_bet_per_number': '每个号码允许的最小下注金额',
|
||||
'field max_bet_per_number': '单号最大下注额',
|
||||
'field_tip max_bet_per_number': '每个号码允许的最大下注金额',
|
||||
'field withdraw_bet_flow_ratio': '提现所需流水倍数',
|
||||
'field_tip withdraw_bet_flow_ratio': '提现前所需完成的投注流水倍数',
|
||||
'field jackpot_max_amount': '大奖审核阈值',
|
||||
'field_tip jackpot_max_amount': '达到该中奖金额后需要进入大奖审核流程',
|
||||
id: 'ID',
|
||||
config_key: '参数键',
|
||||
config_value: '参数值',
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
'status 1': '待开奖',
|
||||
'status 2': '已结算',
|
||||
'status 3': '已退款',
|
||||
'status 4': '已退回',
|
||||
'status 4': '已退回·拒审',
|
||||
'status 5': '待审核',
|
||||
review_title: '中奖审核',
|
||||
review_open: '审核',
|
||||
|
||||
@@ -1,134 +1,197 @@
|
||||
<template>
|
||||
<div class="default-main ba-table-box">
|
||||
<el-alert class="ba-table-alert" v-if="baTable.table.remark" :title="baTable.table.remark" type="info" show-icon />
|
||||
|
||||
<TableHeader
|
||||
:buttons="['refresh', 'add', 'edit', 'delete', 'comSearch', 'quickSearch', 'columnDisplay']"
|
||||
:quick-search-placeholder="t('Quick search placeholder', { fields: t('config.gameConfig.quick Search Fields') })"
|
||||
></TableHeader>
|
||||
|
||||
<Table ref="tableRef"></Table>
|
||||
|
||||
<PopupForm />
|
||||
<div class="default-main">
|
||||
<el-row v-loading="state.loading">
|
||||
<el-col :xs="24" :sm="18">
|
||||
<el-alert v-if="state.remark" :title="state.remark" type="info" show-icon />
|
||||
<el-form
|
||||
v-if="!state.loading"
|
||||
ref="formRef"
|
||||
:model="state.form"
|
||||
:label-position="'top'"
|
||||
@submit.prevent=""
|
||||
@keyup.enter="onSubmit()"
|
||||
>
|
||||
<el-tabs v-model="state.activeTab" type="border-card">
|
||||
<el-tab-pane :label="t('config.gameConfig.form tab label')" name="game_config">
|
||||
<div class="config-form-item" v-for="item in state.configList" :key="item.id">
|
||||
<FormItem
|
||||
:label="resolveFieldLabel(item.config_key)"
|
||||
:type="resolveFormType(item.value_type)"
|
||||
v-model="state.form[item.config_key]"
|
||||
:input-attr="resolveInputAttr(item.value_type)"
|
||||
:tip="resolveFieldTip(item.config_key, item.remark)"
|
||||
/>
|
||||
<div class="config-form-item-name">{{ item.config_key }}</div>
|
||||
</div>
|
||||
<el-button @click="onReset">{{ t('Reset') }}</el-button>
|
||||
<el-button type="primary" :loading="state.submitLoading" @click="onSubmit()">{{ t('Save') }}</el-button>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, provide, useTemplateRef } from 'vue'
|
||||
import { onMounted, reactive, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import PopupForm from './popupForm.vue'
|
||||
import { baTableApi } from '/@/api/common'
|
||||
import { defaultOptButtons } from '/@/components/table'
|
||||
import TableHeader from '/@/components/table/header/index.vue'
|
||||
import Table from '/@/components/table/index.vue'
|
||||
import baTableClass from '/@/utils/baTable'
|
||||
import FormItem from '/@/components/formItem/index.vue'
|
||||
import createAxios from '/@/utils/axios'
|
||||
|
||||
defineOptions({
|
||||
name: 'config/gameConfig',
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
const tableRef = useTemplateRef('tableRef')
|
||||
const optButtons: OptButton[] = defaultOptButtons(['edit', 'delete'])
|
||||
interface GameConfigItem {
|
||||
id: number
|
||||
config_key: string
|
||||
config_value: string
|
||||
value_type: 'string' | 'int' | 'decimal' | 'json'
|
||||
remark: string
|
||||
}
|
||||
|
||||
const baTable = new baTableClass(
|
||||
new baTableApi('/admin/config.GameConfig/'),
|
||||
{
|
||||
pk: 'id',
|
||||
filter: { page: 1, limit: 50 },
|
||||
column: [
|
||||
{ type: 'selection', align: 'center', operator: false },
|
||||
{ label: t('config.gameConfig.id'), prop: 'id', align: 'center', width: 80, operator: 'RANGE', sortable: 'custom' },
|
||||
{
|
||||
label: t('config.gameConfig.config_key'),
|
||||
prop: 'config_key',
|
||||
align: 'center',
|
||||
minWidth: 200,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
operator: 'LIKE',
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.gameConfig.config_value'),
|
||||
prop: 'config_value',
|
||||
align: 'center',
|
||||
minWidth: 160,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
operator: 'LIKE',
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.gameConfig.value_type'),
|
||||
prop: 'value_type',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
operator: 'eq',
|
||||
render: 'tag',
|
||||
custom: {
|
||||
string: 'primary',
|
||||
int: 'success',
|
||||
decimal: 'warning',
|
||||
json: 'info',
|
||||
},
|
||||
replaceValue: {
|
||||
string: t('config.gameConfig.value_type string'),
|
||||
int: t('config.gameConfig.value_type int'),
|
||||
decimal: t('config.gameConfig.value_type decimal'),
|
||||
json: t('config.gameConfig.value_type json'),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t('config.gameConfig.remark'),
|
||||
prop: 'remark',
|
||||
align: 'center',
|
||||
minWidth: 200,
|
||||
operatorPlaceholder: t('Fuzzy query'),
|
||||
operator: 'LIKE',
|
||||
showOverflowTooltip: true,
|
||||
},
|
||||
{
|
||||
label: t('config.gameConfig.create_time'),
|
||||
prop: 'create_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{
|
||||
label: t('config.gameConfig.update_time'),
|
||||
prop: 'update_time',
|
||||
align: 'center',
|
||||
render: 'datetime',
|
||||
operator: 'RANGE',
|
||||
comSearchRender: 'datetime',
|
||||
sortable: 'custom',
|
||||
width: 170,
|
||||
timeFormat: 'yyyy-mm-dd hh:MM:ss',
|
||||
},
|
||||
{ label: t('Operate'), align: 'center', width: 80, render: 'buttons', buttons: optButtons, operator: false, fixed: 'right' },
|
||||
],
|
||||
dblClickNotEditColumn: [undefined],
|
||||
},
|
||||
{
|
||||
defaultItems: { value_type: 'string', remark: '' },
|
||||
const { t, te } = useI18n()
|
||||
const formRef = useTemplateRef('formRef')
|
||||
const api = new baTableApi('/admin/config.GameConfig/')
|
||||
const excludedConfigKeys = new Set(['period_auto_create_enabled', 'period_manual_create_enabled'])
|
||||
|
||||
const state: {
|
||||
loading: boolean
|
||||
submitLoading: boolean
|
||||
activeTab: string
|
||||
remark: string
|
||||
configList: GameConfigItem[]
|
||||
form: Record<string, string | number>
|
||||
} = reactive({
|
||||
loading: true,
|
||||
submitLoading: false,
|
||||
activeTab: 'game_config',
|
||||
remark: '',
|
||||
configList: [],
|
||||
form: {},
|
||||
})
|
||||
|
||||
const getData = () => {
|
||||
state.loading = true
|
||||
api.index({ page: 1, limit: 999 }).then((res) => {
|
||||
const allList = (res.data.list || []) as GameConfigItem[]
|
||||
const list = allList.filter((item) => !excludedConfigKeys.has(item.config_key))
|
||||
state.configList = list
|
||||
state.remark = res.data.remark || ''
|
||||
const nextForm: Record<string, string | number> = {}
|
||||
for (const item of list) {
|
||||
if (item.value_type === 'int' || item.value_type === 'decimal') {
|
||||
const parsed = Number(item.config_value)
|
||||
nextForm[item.config_key] = Number.isNaN(parsed) ? 0 : parsed
|
||||
continue
|
||||
}
|
||||
nextForm[item.config_key] = item.config_value ?? ''
|
||||
}
|
||||
state.form = nextForm
|
||||
}).finally(() => {
|
||||
state.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
const resolveFormType = (valueType: GameConfigItem['value_type']) => {
|
||||
if (valueType === 'json') return 'textarea'
|
||||
if (valueType === 'int' || valueType === 'decimal') return 'number'
|
||||
return 'string'
|
||||
}
|
||||
|
||||
const resolveInputAttr = (valueType: GameConfigItem['value_type']) => {
|
||||
if (valueType === 'json') {
|
||||
return { rows: 5 }
|
||||
}
|
||||
)
|
||||
if (valueType === 'decimal') {
|
||||
return { precision: 4 }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
provide('baTable', baTable)
|
||||
const resolveFieldLabel = (configKey: string) => {
|
||||
const langKey = `config.gameConfig.field ${configKey}`
|
||||
if (te(langKey)) {
|
||||
return t(langKey)
|
||||
}
|
||||
return configKey
|
||||
}
|
||||
|
||||
const resolveFieldTip = (configKey: string, fallbackRemark: string) => {
|
||||
const langKey = `config.gameConfig.field_tip ${configKey}`
|
||||
if (te(langKey)) {
|
||||
return t(langKey)
|
||||
}
|
||||
return fallbackRemark || ''
|
||||
}
|
||||
|
||||
const onSubmit = async () => {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
state.submitLoading = true
|
||||
try {
|
||||
await createAxios({
|
||||
url: '/admin/config.GameConfig/save',
|
||||
method: 'post',
|
||||
data: {
|
||||
items: JSON.parse(JSON.stringify(state.form)),
|
||||
},
|
||||
showSuccessMessage: true,
|
||||
})
|
||||
getData()
|
||||
} finally {
|
||||
state.submitLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
getData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
baTable.table.ref = tableRef.value
|
||||
baTable.mount()
|
||||
baTable.getData()?.then(() => {
|
||||
baTable.initSort()
|
||||
baTable.dragSort()
|
||||
})
|
||||
getData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
border-radius: var(--el-border-radius-base);
|
||||
}
|
||||
.el-tabs--border-card :deep(.el-tabs__header) {
|
||||
background-color: var(--ba-bg-color);
|
||||
border-bottom: none;
|
||||
border-top-left-radius: var(--el-border-radius-base);
|
||||
border-top-right-radius: var(--el-border-radius-base);
|
||||
}
|
||||
.el-tabs--border-card :deep(.el-tabs__item.is-active) {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.el-tabs--border-card :deep(.el-tabs__nav-wrap) {
|
||||
border-top-left-radius: var(--el-border-radius-base);
|
||||
border-top-right-radius: var(--el-border-radius-base);
|
||||
}
|
||||
.config-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.el-form-item {
|
||||
flex: 13;
|
||||
}
|
||||
.config-form-item-name {
|
||||
flex: 3;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-disabled);
|
||||
padding-left: 20px;
|
||||
opacity: 0;
|
||||
}
|
||||
&:hover .config-form-item-name {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
<el-descriptions-item :label="t('game.playRecord.pick_numbers')">{{ formatPickNumbers({}, {}, reviewDialog.row?.pick_numbers) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.total_amount')">{{ formatAmount({}, {}, reviewDialog.row?.total_amount) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.win_amount')">{{ formatAmount({}, {}, reviewDialog.row?.win_amount) }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.status')">{{ reviewDialog.row?.status ? t(`game.playRecord.status ${reviewDialog.row.status}`) : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('game.playRecord.status')">
|
||||
<el-tag v-if="reviewDialog.row?.status !== null && reviewDialog.row?.status !== undefined && reviewDialog.row?.status !== ''" :type="playRecordStatusTagType(reviewDialog.row.status)" effect="dark" size="small">
|
||||
{{ t(`game.playRecord.status ${reviewDialog.row.status}`) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item :label="t('game.playRecord.review_remark')">
|
||||
@@ -81,6 +86,18 @@ function formatPickNumbers(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
function playRecordStatusTagType(status: unknown): 'primary' | 'success' | 'info' | 'warning' | 'danger' {
|
||||
const key = status === null || status === undefined ? '' : String(status)
|
||||
const map: Record<string, 'primary' | 'success' | 'info' | 'warning' | 'danger'> = {
|
||||
'1': 'info',
|
||||
'2': 'success',
|
||||
'3': 'danger',
|
||||
'4': 'warning',
|
||||
'5': 'primary',
|
||||
}
|
||||
return map[key] ?? 'info'
|
||||
}
|
||||
|
||||
function formatAmount(_row: anyObj, _column: any, cellValue: unknown) {
|
||||
if (cellValue === null || cellValue === undefined || cellValue === '') {
|
||||
return '-'
|
||||
@@ -164,11 +181,11 @@ const baTable = new baTableClass(
|
||||
label: t('game.playRecord.status'),
|
||||
prop: 'status',
|
||||
align: 'center',
|
||||
width: 100,
|
||||
minWidth: 118,
|
||||
operator: 'eq',
|
||||
render: 'tag',
|
||||
effect: 'dark',
|
||||
custom: { '1': 'warning', '2': 'success', '3': 'danger', '4': 'info', '5': 'warning' },
|
||||
custom: { '1': 'info', '2': 'success', '3': 'danger', '4': 'warning', '5': 'primary' },
|
||||
replaceValue: {
|
||||
'1': t('game.playRecord.status 1'),
|
||||
'2': t('game.playRecord.status 2'),
|
||||
|
||||
Reference in New Issue
Block a user