Browse Source
- 新增 src/views/exchange/security/index.vue:安全校验总开关 + 兑换限额 + 预警与大额审批 + 防刷配置 - 新增 src/api/exchange/config.js:exchange/config 与 anti/farm 相关接口封装 - 保存配置按有无记录走 updateById / add;校验配置为前端本地一致性校验 - 同步更新 PAGES_TODO / CURRENT_STATUS / DEVELOPMENT_LOG;提交前 eslint 与 build:prod 通过master
5 changed files with 384 additions and 2 deletions
@ -0,0 +1,40 @@ |
|||||||
|
import request from '@/utils/request' |
||||||
|
|
||||||
|
// 获取当前启用的兑换安全配置(无记录时后端返回默认配置;安全开关关闭时返回 null)
|
||||||
|
export function getActiveExchangeConfig() { |
||||||
|
return request({ url: '/business/points/exchange/config/active', method: 'get' }) |
||||||
|
} |
||||||
|
|
||||||
|
// 查询兑换安全开关状态(true-启用,false-关闭)
|
||||||
|
export function getExchangeSwitchStatus() { |
||||||
|
return request({ url: '/business/points/exchange/config/switch/status', method: 'get' }) |
||||||
|
} |
||||||
|
|
||||||
|
// 切换兑换安全开关(管理端)
|
||||||
|
export function toggleExchangeSwitch(enabled) { |
||||||
|
return request({ |
||||||
|
url: '/business/points/exchange/config/switch/toggle', |
||||||
|
method: 'post', |
||||||
|
params: { enabled } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// 新增兑换安全配置
|
||||||
|
export function addExchangeConfig(data) { |
||||||
|
return request({ url: '/business/points/exchange/config/add', method: 'post', data }) |
||||||
|
} |
||||||
|
|
||||||
|
// 修改兑换安全配置(需包含 id)
|
||||||
|
export function updateExchangeConfig(data) { |
||||||
|
return request({ url: '/business/points/exchange/config/updateById', method: 'post', data }) |
||||||
|
} |
||||||
|
|
||||||
|
// 获取启用的防刷分管控配置
|
||||||
|
export function getActiveAntiFarmConfig() { |
||||||
|
return request({ url: '/business/points/anti/farm/active', method: 'get' }) |
||||||
|
} |
||||||
|
|
||||||
|
// 修改防刷分管控配置(需包含 id)
|
||||||
|
export function updateAntiFarmConfig(data) { |
||||||
|
return request({ url: '/business/points/anti/farm/updateById', method: 'post', data }) |
||||||
|
} |
||||||
@ -0,0 +1,333 @@ |
|||||||
|
<template> |
||||||
|
<div v-loading="loading" class="app-container exchange-security"> |
||||||
|
<!-- 安全校验总开关 --> |
||||||
|
<div class="panel"> |
||||||
|
<div class="panel-head"> |
||||||
|
<div> |
||||||
|
<div class="panel-title">兑换安全校验</div> |
||||||
|
<div class="panel-note">开启后,APP 下单前执行:单笔范围 → 每日/每月上限 → 每日次数 → 快速兑换预警 → 大额预警 →(大额审批)→ 防刷校验。</div> |
||||||
|
</div> |
||||||
|
<el-switch v-model="securityEnabled" :loading="switchLoading" @change="handleToggleSwitch" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="two-col"> |
||||||
|
<!-- 兑换限额 --> |
||||||
|
<div class="panel"> |
||||||
|
<div class="panel-title">兑换限额</div> |
||||||
|
<el-form ref="configForm" :model="config" label-width="130px" size="small"> |
||||||
|
<el-form-item label="每人每日上限"> |
||||||
|
<el-input-number v-model="config.dailyLimitPerUser" :min="0" :controls="false" /><span class="unit">积分</span> |
||||||
|
</el-form-item> |
||||||
|
<el-form-item label="每人每月上限"> |
||||||
|
<el-input-number v-model="config.monthlyLimitPerUser" :min="0" :controls="false" /><span class="unit">积分</span> |
||||||
|
</el-form-item> |
||||||
|
<el-form-item label="每人每年上限"> |
||||||
|
<el-input-number v-model="config.yearlyLimitPerUser" :min="0" :controls="false" /><span class="unit">积分</span> |
||||||
|
</el-form-item> |
||||||
|
<el-form-item label="每日兑换次数"> |
||||||
|
<el-input-number v-model="config.dailyOrderCountLimit" :min="0" :controls="false" /><span class="unit">次/人/日</span> |
||||||
|
</el-form-item> |
||||||
|
<el-form-item label="单笔最小积分"> |
||||||
|
<el-input-number v-model="config.singleMinPoints" :min="0" :controls="false" /><span class="unit">积分</span> |
||||||
|
</el-form-item> |
||||||
|
<el-form-item label="单笔最大积分"> |
||||||
|
<el-input-number v-model="config.singleMaxPoints" :min="0" :controls="false" /><span class="unit">积分</span> |
||||||
|
</el-form-item> |
||||||
|
</el-form> |
||||||
|
<div class="panel-note">积分比例及单笔兑换额度由惠服平台统一配置,本端不再维护。</div> |
||||||
|
<div class="panel-actions"> |
||||||
|
<el-button type="primary" size="mini" :loading="saving" @click="saveConfig">保存配置</el-button> |
||||||
|
<el-button size="mini" @click="validateConfig">校验配置</el-button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 预警与大额审批 --> |
||||||
|
<div class="panel"> |
||||||
|
<div class="panel-title">预警与大额审批</div> |
||||||
|
<el-form ref="warningForm" :model="config" label-width="130px" size="small"> |
||||||
|
<el-form-item label="快速兑换阈值"> |
||||||
|
<el-input-number v-model="config.rapidExchangeThreshold" :min="0" :controls="false" /><span class="unit">单 /</span> |
||||||
|
<el-input-number v-model="config.rapidExchangeWindowMinutes" :min="0" :controls="false" /><span class="unit">分钟</span> |
||||||
|
</el-form-item> |
||||||
|
<el-form-item label="大额兑换阈值"> |
||||||
|
<el-input-number v-model="config.largeAmountThreshold" :min="0" :controls="false" /><span class="unit">积分</span> |
||||||
|
</el-form-item> |
||||||
|
<el-form-item label="大额兑换审批"> |
||||||
|
<el-switch v-model="config.largeAmountNeedApproval" :active-value="1" :inactive-value="0" /> |
||||||
|
<span class="unit">开启后大额兑换需人工审批</span> |
||||||
|
</el-form-item> |
||||||
|
</el-form> |
||||||
|
<div class="panel-note">兑换安全校验链:单笔范围 → 每日/每月上限 → 每日次数 → 快速兑换预警 → 大额预警 →(大额审批)→ 防刷校验。</div> |
||||||
|
<div class="panel-actions"> |
||||||
|
<el-button type="primary" size="mini" :loading="saving" @click="saveConfig">保存配置</el-button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<!-- 防刷配置 --> |
||||||
|
<div class="panel"> |
||||||
|
<div class="panel-title">防刷配置(qc_anti_farm_config)</div> |
||||||
|
<el-form ref="antiFarmForm" :model="antiFarm" size="small" label-width="160px"> |
||||||
|
<el-row :gutter="16"> |
||||||
|
<el-col :span="12"> |
||||||
|
<el-form-item label="每日上报次数上限"> |
||||||
|
<el-input-number v-model="antiFarm.dailyReportLimit" :min="0" :controls="false" /><span class="unit">次/人/日</span> |
||||||
|
</el-form-item> |
||||||
|
</el-col> |
||||||
|
<el-col :span="12"> |
||||||
|
<el-form-item label="两次上报最小间隔"> |
||||||
|
<el-input-number v-model="antiFarm.reportIntervalMinutes" :min="0" :controls="false" /><span class="unit">分钟</span> |
||||||
|
</el-form-item> |
||||||
|
</el-col> |
||||||
|
<el-col :span="12"> |
||||||
|
<el-form-item label="同一地点上报上限"> |
||||||
|
<el-input-number v-model="antiFarm.sameLocationReportLimit" :min="0" :controls="false" /><span class="unit">次/日</span> |
||||||
|
</el-form-item> |
||||||
|
</el-col> |
||||||
|
<el-col :span="12"> |
||||||
|
<el-form-item label="每日积分获取上限"> |
||||||
|
<el-input-number v-model="antiFarm.dailyPointsEarnLimit" :min="0" :controls="false" /><span class="unit">积分/日</span> |
||||||
|
</el-form-item> |
||||||
|
</el-col> |
||||||
|
<el-col :span="12"> |
||||||
|
<el-form-item label="同审核人通过上限"> |
||||||
|
<el-input-number v-model="antiFarm.sameReporterAuditLimit" :min="0" :controls="false" /><span class="unit">单/日</span> |
||||||
|
</el-form-item> |
||||||
|
</el-col> |
||||||
|
<el-col :span="12"> |
||||||
|
<el-form-item label="同商品兑换上限"> |
||||||
|
<el-input-number v-model="antiFarm.sameItemDailyLimit" :min="0" :controls="false" /><span class="unit">件/人/日</span> |
||||||
|
</el-form-item> |
||||||
|
</el-col> |
||||||
|
<el-col :span="12"> |
||||||
|
<el-form-item label="同 IP 兑换上限"> |
||||||
|
<el-input-number v-model="antiFarm.ipExchangeDailyLimit" :min="0" :controls="false" /><span class="unit">单/日</span> |
||||||
|
</el-form-item> |
||||||
|
</el-col> |
||||||
|
<el-col :span="12"> |
||||||
|
<el-form-item label="自报自审检测"> |
||||||
|
<el-switch v-model="antiFarm.selfReportCheckEnabled" :active-value="1" :inactive-value="0" /> |
||||||
|
<span class="unit">上报人 ≠ 审核人 + 互审检测</span> |
||||||
|
</el-form-item> |
||||||
|
</el-col> |
||||||
|
</el-row> |
||||||
|
</el-form> |
||||||
|
<div class="panel-actions"> |
||||||
|
<el-button type="primary" size="mini" :loading="savingAnti" @click="saveAntiFarm">保存防刷配置</el-button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import { |
||||||
|
getActiveExchangeConfig, |
||||||
|
getExchangeSwitchStatus, |
||||||
|
toggleExchangeSwitch, |
||||||
|
addExchangeConfig, |
||||||
|
updateExchangeConfig, |
||||||
|
getActiveAntiFarmConfig, |
||||||
|
updateAntiFarmConfig |
||||||
|
} from '@/api/exchange/config' |
||||||
|
|
||||||
|
export default { |
||||||
|
name: 'ExchangeSecurityConfig', |
||||||
|
data() { |
||||||
|
return { |
||||||
|
loading: false, |
||||||
|
saving: false, |
||||||
|
savingAnti: false, |
||||||
|
switchLoading: false, |
||||||
|
securityEnabled: false, |
||||||
|
config: {}, |
||||||
|
antiFarm: {} |
||||||
|
} |
||||||
|
}, |
||||||
|
created() { |
||||||
|
this.loadData() |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
loadData() { |
||||||
|
this.loading = true |
||||||
|
Promise.all([ |
||||||
|
getExchangeSwitchStatus().catch(() => ({ data: false })), |
||||||
|
getActiveExchangeConfig().catch(() => ({ data: null })), |
||||||
|
getActiveAntiFarmConfig().catch(() => ({ data: null })) |
||||||
|
]) |
||||||
|
.then(([statusRes, configRes, antiRes]) => { |
||||||
|
this.securityEnabled = Boolean(statusRes.data) |
||||||
|
this.config = (configRes.data || {}) |
||||||
|
this.antiFarm = (antiRes.data || {}) |
||||||
|
}) |
||||||
|
.catch(() => { |
||||||
|
this.$modal.msgError('安全兑换配置加载失败,请检查后端服务和登录状态') |
||||||
|
}) |
||||||
|
.finally(() => { |
||||||
|
this.loading = false |
||||||
|
}) |
||||||
|
}, |
||||||
|
handleToggleSwitch(enabled) { |
||||||
|
this.switchLoading = true |
||||||
|
toggleExchangeSwitch(enabled) |
||||||
|
.then(() => { |
||||||
|
this.$modal.msgSuccess(enabled ? '已开启兑换安全校验' : '已关闭兑换安全校验') |
||||||
|
this.loadData() |
||||||
|
}) |
||||||
|
.catch(() => { |
||||||
|
this.securityEnabled = !enabled |
||||||
|
this.$modal.msgError('切换失败,请稍后重试') |
||||||
|
}) |
||||||
|
.finally(() => { |
||||||
|
this.switchLoading = false |
||||||
|
}) |
||||||
|
}, |
||||||
|
validateConfig() { |
||||||
|
const errors = [] |
||||||
|
const min = Number(this.config.singleMinPoints) |
||||||
|
const max = Number(this.config.singleMaxPoints) |
||||||
|
const daily = Number(this.config.dailyLimitPerUser) |
||||||
|
const monthly = Number(this.config.monthlyLimitPerUser) |
||||||
|
const yearly = Number(this.config.yearlyLimitPerUser) |
||||||
|
if (!Number.isNaN(min) && !Number.isNaN(max) && min > max) errors.push('单笔最小积分不能大于单笔最大积分') |
||||||
|
if (!Number.isNaN(daily) && !Number.isNaN(monthly) && daily > monthly) errors.push('每人每日上限不能大于每月上限') |
||||||
|
if (!Number.isNaN(monthly) && !Number.isNaN(yearly) && monthly > yearly) errors.push('每人每月上限不能大于每年上限') |
||||||
|
if (errors.length) { |
||||||
|
this.$alert(errors.map(item => `· ${item}`).join('<br>'), '配置校验未通过', { dangerouslyUseHTMLString: true }) |
||||||
|
} else { |
||||||
|
this.$modal.msgSuccess('配置校验通过,未发现冲突') |
||||||
|
} |
||||||
|
}, |
||||||
|
saveConfig() { |
||||||
|
const payload = Object.assign({}, this.config) |
||||||
|
if (payload.id === undefined || payload.id === null || payload.id === '') { |
||||||
|
this.$confirm('当前无兑换安全配置记录,将新增一条配置。是否继续?', '保存配置', { type: 'warning' }) |
||||||
|
.then(() => this.doAddConfig(payload)) |
||||||
|
.catch(() => {}) |
||||||
|
return |
||||||
|
} |
||||||
|
this.doSaveConfig(payload) |
||||||
|
}, |
||||||
|
doSaveConfig(payload) { |
||||||
|
this.saving = true |
||||||
|
updateExchangeConfig(payload) |
||||||
|
.then(() => { |
||||||
|
this.$modal.msgSuccess('配置已保存') |
||||||
|
this.loadData() |
||||||
|
}) |
||||||
|
.catch(() => { |
||||||
|
this.$modal.msgError('保存失败,请稍后重试') |
||||||
|
}) |
||||||
|
.finally(() => { |
||||||
|
this.saving = false |
||||||
|
}) |
||||||
|
}, |
||||||
|
doAddConfig(payload) { |
||||||
|
this.saving = true |
||||||
|
addExchangeConfig(payload) |
||||||
|
.then(() => { |
||||||
|
this.$modal.msgSuccess('配置已新增') |
||||||
|
this.loadData() |
||||||
|
}) |
||||||
|
.catch(() => { |
||||||
|
this.$modal.msgError('新增失败,请稍后重试') |
||||||
|
}) |
||||||
|
.finally(() => { |
||||||
|
this.saving = false |
||||||
|
}) |
||||||
|
}, |
||||||
|
saveAntiFarm() { |
||||||
|
const payload = Object.assign({}, this.antiFarm) |
||||||
|
if (payload.id === undefined || payload.id === null || payload.id === '') { |
||||||
|
this.$modal.msgWarning('未获取到防刷配置记录,无法保存') |
||||||
|
return |
||||||
|
} |
||||||
|
this.savingAnti = true |
||||||
|
updateAntiFarmConfig(payload) |
||||||
|
.then(() => { |
||||||
|
this.$modal.msgSuccess('防刷配置已保存') |
||||||
|
this.loadData() |
||||||
|
}) |
||||||
|
.catch(() => { |
||||||
|
this.$modal.msgError('保存失败,请稍后重试') |
||||||
|
}) |
||||||
|
.finally(() => { |
||||||
|
this.savingAnti = false |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.exchange-security { |
||||||
|
min-height: calc(100vh - 52px); |
||||||
|
} |
||||||
|
|
||||||
|
.panel { |
||||||
|
margin-bottom: 14px; |
||||||
|
padding: 16px 18px; |
||||||
|
background: var(--prototype-panel, #fff); |
||||||
|
border: 1px solid var(--prototype-divider, #f0f0f2); |
||||||
|
border-radius: var(--prototype-radius, 8px); |
||||||
|
} |
||||||
|
|
||||||
|
.panel-head { |
||||||
|
display: flex; |
||||||
|
align-items: center; |
||||||
|
justify-content: space-between; |
||||||
|
gap: 16px; |
||||||
|
} |
||||||
|
|
||||||
|
.panel-title { |
||||||
|
margin-bottom: 12px; |
||||||
|
color: var(--prototype-text, #1a1a2e); |
||||||
|
font-size: 14px; |
||||||
|
font-weight: 600; |
||||||
|
} |
||||||
|
|
||||||
|
.panel-head .panel-title { |
||||||
|
margin-bottom: 4px; |
||||||
|
} |
||||||
|
|
||||||
|
.panel-note { |
||||||
|
margin: 8px 0 0; |
||||||
|
color: var(--prototype-text-muted, #9898ae); |
||||||
|
font-size: 12px; |
||||||
|
line-height: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.panel-actions { |
||||||
|
margin-top: 14px; |
||||||
|
} |
||||||
|
|
||||||
|
.two-col { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: 1fr 1fr; |
||||||
|
gap: 14px; |
||||||
|
} |
||||||
|
|
||||||
|
.two-col .panel { |
||||||
|
margin-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.unit { |
||||||
|
margin-left: 8px; |
||||||
|
color: var(--prototype-text-muted, #9898ae); |
||||||
|
font-size: 12px; |
||||||
|
} |
||||||
|
|
||||||
|
::v-deep .el-input-number { |
||||||
|
width: 140px; |
||||||
|
} |
||||||
|
|
||||||
|
@media (max-width: 1100px) { |
||||||
|
.two-col { |
||||||
|
grid-template-columns: 1fr; |
||||||
|
} |
||||||
|
|
||||||
|
.two-col .panel { |
||||||
|
margin-bottom: 14px; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
||||||
Loading…
Reference in new issue