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
DU 4 days ago
parent
commit
2b98fa822c
  1. 1
      docs/development/CURRENT_STATUS.md
  2. 8
      docs/development/DEVELOPMENT_LOG.md
  3. 4
      docs/development/PAGES_TODO.md
  4. 40
      src/api/exchange/config.js
  5. 333
      src/views/exchange/security/index.vue

1
docs/development/CURRENT_STATUS.md

@ -39,6 +39,7 @@ @@ -39,6 +39,7 @@
| 隐患台账 | `src/views/hazard/ledger/index.vue` | `hazard/ledger/index` | 已完成新版 Card Table、五态筛选/Tag、分页、详情弹窗和审核入口;列表已切换为 `/flowable/hazard/todoList`;单位筛选按角色使用 `factoryList`/`myFactory`;附件九宫格支持大图预览;平台同步列暂时隐藏;同步重试等待后端接口 |
| 积分管理 | `src/views/points/management/index.vue` | `points/management/index` | 已完成积分规则 Tab、统一 Card 列表、厂区列、分页、详情、新增、启停和删除;超管可筛选/选择厂区,厂区管理员可管理本厂区规则。**流水冲正 Tab 已开发(2026-10-15,待测试)**;申诉处理 Tab 已启用(复用独立「申诉台账」页同一组件 `AppealLedgerPanel`);手动调整 Tab 因 `balance/adjust` 接口定义不完整暂缓 |
| 申诉台账 | `src/views/points/appeal/index.vue` | `points/appeal/index` | 已开发(2026-10-15),待测试。独立页(抄隐患台账 + 审批):列表 `appeal/todoList`、详情 `appeal/detail/{id}`、审批 `appeal/completeFlow`(taskId 由 `flowable/busiInfo/getProcessDetail` 补全)。待联调确认:completeFlow 的 taskId 来源、改判补分 `adjustPoints` 传参 |
| 安全兑换管理 | `src/views/exchange/security/index.vue` | `exchange/security/index` | 已开发(2026-10-15),待测试。安全校验总开关 + 兑换限额 + 预警与大额审批 + 防刷配置;接口 `exchange/config/*`、`anti/farm/*`。“校验配置”为前端本地一致性校验 |
| 隐患流程配置 | `src/views/flowable/hazardConfig/index.vue` | `flowable/hazardConfig/index` | 已完成列表、筛选、分页、新增、修改、状态 Tag、删除和厂区权限展示;新增“流程类型”字段(`bizType`,字典 `flow_biz_type`)并在列表以 Tag 展示;分页按真实 `data.list/data.total` 解析 |
| 切换日志 | `src/views/flowable/switchLog/index.vue` | `flowable/switchLog/index` | 空白页面,无明确列表接口 |

8
docs/development/DEVELOPMENT_LOG.md

@ -1,5 +1,13 @@ @@ -1,5 +1,13 @@
# 开发日志
## 2026-10-15:新增 Web 安全兑换管理页面(W5)
- 新增 `src/views/exchange/security/index.vue`:安全校验总开关(`switch/status` + `switch/toggle`)、兑换限额(每日/每月/每年/次数/单笔最小最大)、预警与大额审批(快速兑换阈值+窗口、大额阈值、大额审批开关)、防刷配置(上报/积分获取/兑换/自报自审)。
- 新增 `src/api/exchange/config.js`:`exchange/config/active`·`switch/status`·`switch/toggle`·`add`·`updateById`、`anti/farm/active`·`updateById`。
- “保存配置”:有记录走 `updateById`,无记录提示后走 `add`;“校验配置”为**前端本地一致性校验**(单笔最小≤最大、每日≤每月≤每年),无后端校验配置接口。
- 菜单(需后端配置):一级「安全兑换管理」,路由 `exchange/security`,组件 `exchange/security/index`(`/exchange/security`)。
- 验证:`eslint --no-ignore` 通过、`npm run build:prod` 通过(仅既有 warning)。
## 2026-10-15:申诉处理 Tab 复用申诉台账组件
- 将申诉台账核心逻辑抽成可复用组件 `src/views/points/appeal/components/AppealLedgerPanel.vue`(列表/详情/审批)。

4
docs/development/PAGES_TODO.md

@ -38,7 +38,7 @@ @@ -38,7 +38,7 @@
| 14 | 积分与商城 | 商城运营 › 纯积分兑换管理 | ❌ | ⛔ | ⛔ | 同上 |
| 15 | 积分与商城 | 商城运营 › 日常积分兑换管理 | ❌ | ⛔ | ⛔ | 同上 |
| 16 | 积分与商城 | 商城运营 › 日常积分兑换专区 | ❌ | ⛔ | ⛔ | 同上 |
| 17 | 积分与商城 | 安全兑换管理(限额/大额审批/防刷 4 维度/校验) | ❌ | ✅ | ✅ | `exchange/config/active`、`validate`、`switch/status`、`switch/toggle`、`updateById`([兑换安全配置](../api/modules/11-exchange-config.md));防刷 `anti/farm/active`、`detectAll`、`detectSelfAudit`([防刷分管控](../api/modules/13-anti-farm-config.md)) |
| 17 | 安全兑换管理 | 安全兑换管理(总开关/限额/大额审批/防刷配置) | ⚠️ 待测试 | ✅ | ✅ | **已开发(2026-10-15)**:`src/views/exchange/security/index.vue` + `src/api/exchange/config.js`。接口 `exchange/config/active`·`switch/status`·`switch/toggle`·`add`·`updateById`、`anti/farm/active`·`updateById`。注:“校验配置”为前端本地一致性校验(无后端校验配置接口) |
| 18 | 分析与风控 | 数据分析(时效漏斗/排行/分类分布/导出;**无热力图、无顶部筛选列**) | ❌ | ✅ | ✅ | 20 个接口齐全([数据分析](../api/modules/15-data-analysis.md));“超期未处理表”可用隐患待办列表拼接。**页面调整(2026-10-15 甲方)**:去掉隐患热力图、去掉顶部筛选列;“处理时效漏斗”与“积分排行”改为左右布局占一行 |
| 19 | 分析与风控 | 风控与审计(Tab1 风控预警 + Tab2 审计日志) | ❌ | ⚠️ | ⚠️ | 审计日志 ✅:`audit/log/getListByPage`、`audit/log/biz/{bizId}`([操作审计日志](../api/modules/16-audit-log.md));**风控预警列表 + 处置(放行/冻结 24h/黑名单)无接口**,Tab1 需等后端 |
| 20 | 流程管理 | 隐患流程配置 | ✅ | ✅ | — | [flowable/hazardConfig](../../src/views/flowable/hazardConfig/index.vue) |
@ -202,7 +202,7 @@ @@ -202,7 +202,7 @@
| W2 | Web | 申诉台账(独立页,抄隐患台账 + 审批) | `appeal/todoList`、`appeal/detail/{id}`、`appeal/completeFlow`、`flowable/busiInfo/getProcessDetail` | `src/views/points/appeal/index.vue` + `src/api/points/appeal.js` | 高 | ◐ 待测试 | **已开发(2026-10-15)**,待测试。菜单:一级「积分与商城」→「申诉台账」,路由 `points/appeal`,组件 `points/appeal/index`(`/points/appeal`)。待联调确认:completeFlow 的 taskId 来源、改判补分 `adjustPoints` 传参(当前放 `variables`) |
| W3 | Web | 积分管理 · 手动调整 Tab | `balance/adjust`、`balance/getByUserId/{userId}` | `src/views/points/management/index.vue` + `src/api/points/balance.js` | 高 | ⛔ 需确认 | **接口定义不完整**:`balance/adjust` 文档仅声明 `application/x-www-form-urlencoded` + 单个整数,缺 `userId`/`points`/原因字段结构,无法开工;**后端计划 2026-10-16 更新该接口并补参数**,更新后即可开发 |
| W4 | Web | 积分管理 · 流水冲正 Tab | `transaction/getListByPage`、`transaction/reverse/{logId}` | `src/views/points/management/components/TransactionReversePanel.vue` + `src/api/points/transaction.js` | 中 | ◐ 待测试 | **已开发(2026-10-15)**,待测试。积分管理第 4 个 Tab;仅 status=1(有效)可冲正,二次确认。流水无用户姓名字段,列表显示 userId |
| W5 | Web | 安全兑换管理 | `exchange/config/active`·`validate`·`switch/status`·`switch/toggle`·`updateById`、`anti/farm/active`·`detectAll`·`detectSelfAudit` | `src/views/exchange/security/index.vue` + `src/api/exchange/config.js` | 中 | ☐ | 字段见 `ExchangeConfig` / `AntiFarmConfig` |
| W5 | Web | 安全兑换管理 | `exchange/config/active`·`switch/status`·`switch/toggle`·`add`·`updateById`、`anti/farm/active`·`updateById` | `src/views/exchange/security/index.vue` + `src/api/exchange/config.js` | 中 | ◐ 待测试 | **已开发(2026-10-15)**,待测试。菜单:一级「安全兑换管理」,路由 `exchange/security`,组件 `exchange/security/index`(`/exchange/security`) |
| W6 | Web | 数据分析 | `analysis/stats/*`、`ranking/*`、`timeliness/level`、`report/monthly`(+`/export`)、`stats/category`·`stats/level` | `src/views/analysis/index.vue` + `src/api/analysis/index.js` | 中 | ☐ | **页面调整(2026-10-15)**:① 去掉隐患热力图(`heatmap/*` 接口暂不接);② 去掉顶部筛选列;③「处理时效漏斗」与「积分排行」改为左右布局占一行。图表用已装 `echarts` |
| A1 | APP | 设置(推送/声音/震动/关于/清缓存/退出) | 纯本地(AsyncStorage) | `src/screens/home/SettingsScreen.tsx` | 中 | ☐ | 参考原型 `scr-settings` |
| A2 | APP | 我的兑换 | `exchange/order/myList` | `src/screens/home/MyExchangeScreen.tsx` + `src/api/exchange.ts` | 中 | ☐ | 入口:我的 → 我的兑换 |

40
src/api/exchange/config.js

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

333
src/views/exchange/security/index.vue

@ -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…
Cancel
Save