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 @@
@@ -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 @@
@@ -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