更新增加统计【活动期间输赢总数据】

This commit is contained in:
2026-07-27 15:41:31 +08:00
parent f2543b9aa4
commit f91eb186ca
19 changed files with 375 additions and 122 deletions

View File

@@ -57,6 +57,6 @@ export default {
},
playxConfig: {
title: 'About this page',
desc: 'Configure daily and lifetime loss-to-points sources (switches and ratios), mall open date, unlock ratio, and points-to-cash ratio. Open date changes take effect the next calendar day.',
desc: 'Split into Event and General tabs: event start/end dates (effective next day; no conversion for end-day business data from the day after end), point sources and lifetime batch job; unlock and cash ratios under General.',
},
}

View File

@@ -1,9 +1,12 @@
export default {
tab_event: 'Event settings',
tab_regular: 'General settings',
section_event_period: 'Event period',
section_daily_source: 'Source 1: yesterday net win/loss',
daily_points_enabled: 'Enable daily loss to points',
daily_points_enabled_tip: 'Convert yesterday net win/loss from daily push to locked points; only losses (negative net) count.',
return_ratio: 'Yesterday loss conversion ratio',
return_ratio_tip: 'On daily push: converted points = floor(|yesterday net|) × ratio (losses only, non-negative integer). Requires mall open date and enabled source.',
return_ratio_tip: 'On daily push: converted points = floor(|yesterday net|) × ratio (losses only). Requires event window and enabled source.',
section_lifetime_source: 'Source 2: lifetime net win/loss',
lifetime_points_enabled: 'Enable lifetime loss to points',
lifetime_points_enabled_tip: 'Uses the latest daily push row per member; only losses. Skips when lifetime field is missing.',
@@ -14,12 +17,16 @@ export default {
calculate_lifetime_confirm: 'Recalculate all members with the current lifetime ratio (reverses previous lifetime conversion first). Continue?',
calculate_lifetime_success: 'Lifetime conversion done: {processed} processed, {updated} updated, {skipped} skipped',
section_mall_open: 'Mall open date',
mall_open_date: 'Mall open date',
mall_open_date_tip: 'Takes effect the next calendar day. E.g. set Aug 1: from Aug 2, daily push rows with business date on/after Aug 1 convert yesterday loss to points.',
active_mall_open_date: 'Active open date',
mall_open_date: 'Event start date',
mall_open_date_tip: 'Takes effect the next calendar day. From then, daily push rows with business date on/after start date convert yesterday loss.',
active_mall_open_date: 'Active start date',
mall_event_end_date: 'Event end date',
mall_event_end_date_tip: 'Takes effect the next day; leave empty for no end. On the day after end, rows for the end business date no longer convert points or unlock cap; later business dates are excluded too.',
active_event_end_date: 'Active end date',
event_end_before_start: 'End date cannot be earlier than start date',
section_other: 'Other settings',
unlock_ratio: 'Unlock ratio (unlock_ratio)',
unlock_ratio_tip: 'Daily push: daily claim cap = yesterday total deposit × this ratio.',
unlock_ratio_tip: 'Daily push: daily claim cap = yesterday total deposit × this ratio (within event window).',
points_to_cash_ratio: 'Points to cash ratio',
points_to_cash_ratio_tip: 'For withdrawable cash display: cash ≈ available points × this ratio.',
}

View File

@@ -11,6 +11,7 @@ export default {
today_limit_date: 'Daily cap date',
points_claim_cap: 'Lifetime claim cap',
total_points_claimed: 'Total points claimed',
event_period_win_loss_net: 'Event period net W/L (adds while open, pauses when closed, resumes on reopen)',
create_time: 'Created at',
update_time: 'Updated at',
'quick Search Fields': 'ID, PlayX user ID, username, phone',

View File

@@ -57,6 +57,6 @@ export default {
},
playxConfig: {
title: '页面说明',
desc: '配置昨日净输赢、终身总输赢两种积分来源(开关与兑换比例)、商城开放日,以及解锁比例与积分折算现金比例;保存后按规则生效(开放日隔日生效)。',
desc: '分为「活动配置」与「常规配置」:活动开始/结束日(隔日生效,结束日次日起不再折算该日业务数据的昨日净输赢与解锁上限)、两种积分来源及终身一键计算;常规项为解锁比例与积分折现比例。',
},
}

View File

@@ -1,9 +1,12 @@
export default {
tab_event: '活动配置',
tab_regular: '常规配置',
section_event_period: '活动时间',
section_daily_source: '积分来源一:昨日净输赢',
daily_points_enabled: '启用昨日净输赢转积分',
daily_points_enabled_tip: '根据每日推送中的「昨日净输赢」折算待领取积分;仅统计会员输的钱(净输赢为负),赢的不计。',
return_ratio: '昨日净输赢兑换比例',
return_ratio_tip: '每日推送入库时:转换积分 = floor(|昨日净输赢|) × 本比例(仅负值,结果为非负整数)。需满足商城开放日规则且本来源已启用。',
return_ratio_tip: '每日推送入库时:转换积分 = floor(|昨日净输赢|) × 本比例(仅负值,结果为非负整数)。需满足活动时间规则且本来源已启用。',
section_lifetime_source: '积分来源二:终身总输赢',
lifetime_points_enabled: '启用终身总输赢转积分',
lifetime_points_enabled_tip: '使用每日推送中每位会员最新一条记录的「终身总输赢」字段;仅统计输的钱。第三方未传该字段时跳过。',
@@ -14,12 +17,16 @@ export default {
calculate_lifetime_confirm: '将按当前终身总输赢兑换比例重新折算全部会员积分,已折算部分会先扣回再按新比例增加。是否继续?',
calculate_lifetime_success: '终身总输赢积分折算完成:共处理 {processed} 条,更新 {updated} 条,跳过 {skipped} 条',
section_mall_open: '商城开放日',
mall_open_date: '商城开放日',
mall_open_date_tip: '设置后隔日生效。例如设为 8 月 1 日,则从 8 月 2 日起对业务日期为 8 月 1 日及之后的每日推送执行「昨日净输赢」转积分T+1:开放日前一日产生的输赢在开放日次日开始折算)。',
active_mall_open_date: '当前生效的开日',
mall_open_date: '活动开始日',
mall_open_date_tip: '设置后隔日生效。从生效日起对业务日期不早于开始日的每日推送执行「昨日净输赢」转积分T+1。',
active_mall_open_date: '当前生效的开日',
mall_event_end_date: '活动结束日',
mall_event_end_date_tip: '设置后隔日生效,可留空表示不限制结束。结束日次日不再对结束日当天业务数据的昨日净输赢折算积分与解锁上限;结束日之后的业务日期也不再折算。',
active_event_end_date: '当前生效的结束日',
event_end_before_start: '活动结束日不能早于开始日',
section_other: '其他参数',
unlock_ratio: '解锁比例unlock_ratio',
unlock_ratio_tip: '每日推送中:今日可领取上限 = 昨日总充值 × 本比例。',
unlock_ratio_tip: '每日推送中:今日可领取上限 = 昨日总充值 × 本比例(受活动时间规则约束)。',
points_to_cash_ratio: '积分折算现金比例',
points_to_cash_ratio_tip: '用于资产接口中「可提现现金」等展示:现金 ≈ 可用积分 × 本比例。',
}

View File

@@ -11,6 +11,7 @@ export default {
today_limit_date: '今日上限日期',
points_claim_cap: '领取积分上限',
total_points_claimed: '累计已领取积分',
event_period_win_loss_net: '活动期累计净输赢(开放时累加,关闭暂停,再开续累)',
create_time: '创建时间',
update_time: '修改时间',
'quick Search Fields': 'ID、playX用户ID、用户名、手机号',

View File

@@ -93,7 +93,7 @@ const baTable = new baTableClass(
prop: 'status',
align: 'center',
effect: 'dark',
custom: { PENDING: 'success', COMPLETED: 'primary', SHIPPED: 'info', REJECTED: 'loading' },
custom: { PENDING: 'success', COMPLETED: 'primary', SHIPPED: 'info', REJECTED: 'danger' },
minWidth: 100,
operator: 'eq',
sortable: false,

View File

@@ -14,96 +14,117 @@
@submit.prevent=""
@keyup.enter="onSubmit()"
>
<el-divider content-position="left">{{ t('mall.playxConfig.section_daily_source') }}</el-divider>
<el-form-item :label="t('mall.playxConfig.daily_points_enabled')" prop="daily_points_enabled">
<el-switch v-model="form.daily_points_enabled" />
<div class="form-tip">{{ t('mall.playxConfig.daily_points_enabled_tip') }}</div>
</el-form-item>
<el-form-item :label="t('mall.playxConfig.return_ratio')" prop="return_ratio">
<el-input-number
v-model="form.return_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
:disabled="!form.daily_points_enabled"
/>
<div class="form-tip">{{ t('mall.playxConfig.return_ratio_tip') }}</div>
</el-form-item>
<el-tabs v-model="activeTab" class="playx-config-tabs">
<el-tab-pane :label="t('mall.playxConfig.tab_event')" name="event">
<el-divider content-position="left">{{ t('mall.playxConfig.section_event_period') }}</el-divider>
<el-form-item :label="t('mall.playxConfig.mall_open_date')" prop="mall_open_date">
<el-date-picker
v-model="form.mall_open_date"
type="date"
value-format="YYYY-MM-DD"
class="w100"
:placeholder="t('Please select field', { field: t('mall.playxConfig.mall_open_date') })"
/>
<div class="form-tip">{{ t('mall.playxConfig.mall_open_date_tip') }}</div>
<div v-if="activeMallOpenDate" class="form-tip">
{{ t('mall.playxConfig.active_mall_open_date') }}{{ activeMallOpenDate }}
</div>
</el-form-item>
<el-form-item :label="t('mall.playxConfig.mall_event_end_date')" prop="mall_event_end_date">
<el-date-picker
v-model="form.mall_event_end_date"
type="date"
value-format="YYYY-MM-DD"
class="w100"
clearable
:placeholder="t('Please select field', { field: t('mall.playxConfig.mall_event_end_date') })"
/>
<div class="form-tip">{{ t('mall.playxConfig.mall_event_end_date_tip') }}</div>
<div v-if="activeEventEndDate" class="form-tip">
{{ t('mall.playxConfig.active_event_end_date') }}{{ activeEventEndDate }}
</div>
</el-form-item>
<el-divider content-position="left">{{ t('mall.playxConfig.section_lifetime_source') }}</el-divider>
<el-form-item :label="t('mall.playxConfig.lifetime_points_enabled')" prop="lifetime_points_enabled">
<el-switch v-model="form.lifetime_points_enabled" />
<div class="form-tip">{{ t('mall.playxConfig.lifetime_points_enabled_tip') }}</div>
</el-form-item>
<el-form-item :label="t('mall.playxConfig.lifetime_points_ratio')" prop="lifetime_points_ratio">
<el-input-number
v-model="form.lifetime_points_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
:disabled="!form.lifetime_points_enabled"
/>
<div class="form-tip">{{ t('mall.playxConfig.lifetime_points_ratio_tip') }}</div>
</el-form-item>
<el-form-item>
<el-button
type="warning"
:loading="calculating"
:disabled="!form.lifetime_points_enabled"
@click="onCalculateLifetime()"
>
{{ t('mall.playxConfig.calculate_lifetime_btn') }}
</el-button>
<div class="form-tip">{{ t('mall.playxConfig.calculate_lifetime_tip') }}</div>
</el-form-item>
<el-divider content-position="left">{{ t('mall.playxConfig.section_daily_source') }}</el-divider>
<el-form-item :label="t('mall.playxConfig.daily_points_enabled')" prop="daily_points_enabled">
<el-switch v-model="form.daily_points_enabled" />
<div class="form-tip">{{ t('mall.playxConfig.daily_points_enabled_tip') }}</div>
</el-form-item>
<el-form-item :label="t('mall.playxConfig.return_ratio')" prop="return_ratio">
<el-input-number
v-model="form.return_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
:disabled="!form.daily_points_enabled"
/>
<div class="form-tip">{{ t('mall.playxConfig.return_ratio_tip') }}</div>
</el-form-item>
<el-divider content-position="left">{{ t('mall.playxConfig.section_mall_open') }}</el-divider>
<el-form-item :label="t('mall.playxConfig.mall_open_date')" prop="mall_open_date">
<el-date-picker
v-model="form.mall_open_date"
type="date"
value-format="YYYY-MM-DD"
class="w100"
:placeholder="t('Please select field', { field: t('mall.playxConfig.mall_open_date') })"
/>
<div class="form-tip">{{ t('mall.playxConfig.mall_open_date_tip') }}</div>
<div v-if="activeMallOpenDate" class="form-tip">
{{ t('mall.playxConfig.active_mall_open_date') }}{{ activeMallOpenDate }}
</div>
</el-form-item>
<el-divider content-position="left">{{ t('mall.playxConfig.section_lifetime_source') }}</el-divider>
<el-form-item :label="t('mall.playxConfig.lifetime_points_enabled')" prop="lifetime_points_enabled">
<el-switch v-model="form.lifetime_points_enabled" />
<div class="form-tip">{{ t('mall.playxConfig.lifetime_points_enabled_tip') }}</div>
</el-form-item>
<el-form-item :label="t('mall.playxConfig.lifetime_points_ratio')" prop="lifetime_points_ratio">
<el-input-number
v-model="form.lifetime_points_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
:disabled="!form.lifetime_points_enabled"
/>
<div class="form-tip">{{ t('mall.playxConfig.lifetime_points_ratio_tip') }}</div>
</el-form-item>
<el-form-item>
<el-button
type="warning"
:loading="calculating"
:disabled="!form.lifetime_points_enabled"
@click="onCalculateLifetime()"
>
{{ t('mall.playxConfig.calculate_lifetime_btn') }}
</el-button>
<div class="form-tip">{{ t('mall.playxConfig.calculate_lifetime_tip') }}</div>
</el-form-item>
</el-tab-pane>
<el-divider content-position="left">{{ t('mall.playxConfig.section_other') }}</el-divider>
<el-form-item :label="t('mall.playxConfig.unlock_ratio')" prop="unlock_ratio">
<el-input-number
v-model="form.unlock_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
/>
<div class="form-tip">{{ t('mall.playxConfig.unlock_ratio_tip') }}</div>
</el-form-item>
<el-form-item :label="t('mall.playxConfig.points_to_cash_ratio')" prop="points_to_cash_ratio">
<el-input-number
v-model="form.points_to_cash_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
/>
<div class="form-tip">{{ t('mall.playxConfig.points_to_cash_ratio_tip') }}</div>
</el-form-item>
<el-form-item>
<el-tab-pane :label="t('mall.playxConfig.tab_regular')" name="regular">
<el-divider content-position="left">{{ t('mall.playxConfig.section_other') }}</el-divider>
<el-form-item :label="t('mall.playxConfig.unlock_ratio')" prop="unlock_ratio">
<el-input-number
v-model="form.unlock_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
/>
<div class="form-tip">{{ t('mall.playxConfig.unlock_ratio_tip') }}</div>
</el-form-item>
<el-form-item :label="t('mall.playxConfig.points_to_cash_ratio')" prop="points_to_cash_ratio">
<el-input-number
v-model="form.points_to_cash_ratio"
:min="0"
:max="100"
:step="0.01"
:precision="2"
controls-position="right"
class="w100"
/>
<div class="form-tip">{{ t('mall.playxConfig.points_to_cash_ratio_tip') }}</div>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-form-item class="playx-config-submit">
<el-button type="primary" :loading="submitting" @click="onSubmit()">{{ t('Save') }}</el-button>
</el-form-item>
</el-form>
@@ -129,7 +150,9 @@ const loading = ref(true)
const submitting = ref(false)
const calculating = ref(false)
const remark = ref('')
const activeTab = ref('event')
const activeMallOpenDate = ref('')
const activeEventEndDate = ref('')
const form = reactive({
return_ratio: 0.1,
@@ -139,13 +162,26 @@ const form = reactive({
lifetime_points_enabled: false,
lifetime_points_ratio: 0.1,
mall_open_date: '' as string | null,
mall_event_end_date: '' as string | null,
})
const validateEventDates = (_rule: unknown, _value: unknown, callback: (err?: Error) => void) => {
const start = form.mall_open_date
const end = form.mall_event_end_date
if (start && end && end < start) {
callback(new Error(t('mall.playxConfig.event_end_before_start')))
return
}
callback()
}
const rules: FormRules = {
return_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.return_ratio') }) }],
unlock_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.unlock_ratio') }) }],
points_to_cash_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.points_to_cash_ratio') }) }],
lifetime_points_ratio: [{ required: true, message: t('Please input field', { field: t('mall.playxConfig.lifetime_points_ratio') }) }],
mall_event_end_date: [{ validator: validateEventDates, trigger: 'change' }],
mall_open_date: [{ validator: validateEventDates, trigger: 'change' }],
}
const loadData = () => {
@@ -155,6 +191,7 @@ const loadData = () => {
remark.value = res.data.remark ?? ''
const row = res.data.row ?? {}
activeMallOpenDate.value = row.active_mall_open_date ?? ''
activeEventEndDate.value = row.active_event_end_date ?? ''
if (row.return_ratio !== undefined && row.return_ratio !== null) {
form.return_ratio = parseFloat(String(row.return_ratio))
}
@@ -170,6 +207,7 @@ const loadData = () => {
form.lifetime_points_ratio = parseFloat(String(row.lifetime_points_ratio))
}
form.mall_open_date = row.mall_open_date ? String(row.mall_open_date).slice(0, 10) : null
form.mall_event_end_date = row.mall_event_end_date ? String(row.mall_event_end_date).slice(0, 10) : null
})
.finally(() => {
loading.value = false
@@ -188,6 +226,7 @@ const onSubmit = () => {
lifetime_points_enabled: form.lifetime_points_enabled ? 1 : 0,
lifetime_points_ratio: form.lifetime_points_ratio,
mall_open_date: form.mall_open_date || null,
mall_event_end_date: form.mall_event_end_date || null,
})
.then(() => {
loadData()
@@ -236,4 +275,11 @@ onMounted(() => {
color: var(--el-text-color-secondary);
line-height: 1.5;
}
.playx-config-tabs {
margin-bottom: 8px;
}
.playx-config-submit {
margin-top: 16px;
margin-bottom: 0;
}
</style>

View File

@@ -116,6 +116,14 @@ const baTable = new baTableClass(
operator: 'RANGE',
sortable: true,
},
{
label: t('mall.userAsset.event_period_win_loss_net'),
prop: 'event_period_win_loss_net',
align: 'center',
minWidth: 160,
operator: 'RANGE',
sortable: true,
},
{
label: t('mall.userAsset.today_limit_date'),
prop: 'today_limit_date',

View File

@@ -77,6 +77,13 @@
:input-attr="{ step: 1, min: 0 }"
:placeholder="t('Please input field', { field: t('mall.userAsset.today_limit') })"
/>
<FormItem
:label="t('mall.userAsset.event_period_win_loss_net')"
type="number"
v-model="baTable.form.items!.event_period_win_loss_net"
prop="event_period_win_loss_net"
:input-attr="{ disabled: true, step: 0.01 }"
/>
<FormItem
:label="t('mall.userAsset.today_claimed')"
type="number"