Browse Source
- 新增 src/views/risk/index.vue:风控预警 Tab(异常行为检测、自报自审检测)+ 审计日志 Tab(筛选/分页/展开前后数据) - 新增 src/api/risk/index.js:audit/log 与 anti/farm 检测接口封装 - 风控预警列表与处置无后端接口,保留占位 - 同步更新 PAGES_TODO / CURRENT_STATUS / DEVELOPMENT_LOG;提交前 eslint 与 build:prod 通过master
5 changed files with 408 additions and 2 deletions
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
import request from '@/utils/request' |
||||
|
||||
// 审计日志分页查询(操作类型 / 操作人 / 业务单号)
|
||||
export function listAuditLogs(params) { |
||||
return request({ url: '/business/audit/log/getListByPage', method: 'get', params }) |
||||
} |
||||
|
||||
// 按业务单号查询关联审计日志(时间倒序)
|
||||
export function listAuditLogsByBiz(bizId) { |
||||
return request({ url: '/business/audit/log/biz/' + bizId, method: 'get' }) |
||||
} |
||||
|
||||
// 异常行为一键检测(指定用户,聚合上报/积分获取/兑换 3 类校验)
|
||||
export function detectAbnormal(userId) { |
||||
return request({ url: '/business/points/anti/farm/detectAll', method: 'get', params: { userId }}) |
||||
} |
||||
|
||||
// 自报自审行为检测(上报人 = 审核人?)
|
||||
export function detectSelfAudit(reporterId, auditorId) { |
||||
return request({ |
||||
url: '/business/points/anti/farm/detectSelfAudit', |
||||
method: 'get', |
||||
params: { reporterId, auditorId } |
||||
}) |
||||
} |
||||
@ -0,0 +1,372 @@
@@ -0,0 +1,372 @@
|
||||
<template> |
||||
<div class="app-container risk-audit"> |
||||
<el-tabs v-model="activeTab"> |
||||
<!-- 风控预警 --> |
||||
<el-tab-pane label="风控预警" name="alert"> |
||||
<div class="panel"> |
||||
<div class="panel-title">异常行为检测</div> |
||||
<el-form :inline="true" size="small"> |
||||
<el-form-item label="检测用户"> |
||||
<select-tree |
||||
ref="detectUserTree" |
||||
type="tree_person" |
||||
:chenk-parent-node="false" |
||||
:echo-back="detectForm.userId" |
||||
placeholder="请选择要检测的用户" |
||||
style="width:240px" |
||||
@OrgNodeClick="handleDetectUserSelect" |
||||
/> |
||||
</el-form-item> |
||||
<el-form-item> |
||||
<el-button type="primary" size="mini" :loading="detecting" @click="handleDetect">执行异常行为检测</el-button> |
||||
</el-form-item> |
||||
</el-form> |
||||
<div v-if="detectResult" class="detect-result"> |
||||
<div class="detect-result__overall" :class="detectResult.overallPassed ? 'is-pass' : 'is-fail'"> |
||||
综合结果:{{ detectResult.overallPassed ? '未发现异常' : '存在异常' }} |
||||
</div> |
||||
<div v-for="check in detectChecks" :key="check.key" class="detect-result__item"> |
||||
<span class="detect-result__name">{{ check.name }}</span> |
||||
<el-tag :type="check.passed ? 'success' : 'danger'" size="small">{{ check.passed ? '通过' : '异常' }}</el-tag> |
||||
<span class="detect-result__reason">{{ check.reason || '—' }}</span> |
||||
</div> |
||||
</div> |
||||
<div v-else class="panel-note">选择用户后点击检测,返回上报防刷、积分获取防刷、兑换防刷三类校验结果。</div> |
||||
</div> |
||||
|
||||
<div class="panel"> |
||||
<div class="panel-title">自报自审检测</div> |
||||
<el-form :inline="true" size="small"> |
||||
<el-form-item label="上报人"> |
||||
<select-tree |
||||
ref="reporterTree" |
||||
type="tree_person" |
||||
:chenk-parent-node="false" |
||||
:echo-back="selfAuditForm.reporterId" |
||||
placeholder="请选择上报人" |
||||
style="width:220px" |
||||
@OrgNodeClick="node => handleSelfAuditSelect('reporterId', node)" |
||||
/> |
||||
</el-form-item> |
||||
<el-form-item label="审核人"> |
||||
<select-tree |
||||
ref="auditorTree" |
||||
type="tree_person" |
||||
:chenk-parent-node="false" |
||||
:echo-back="selfAuditForm.auditorId" |
||||
placeholder="请选择审核人" |
||||
style="width:220px" |
||||
@OrgNodeClick="node => handleSelfAuditSelect('auditorId', node)" |
||||
/> |
||||
</el-form-item> |
||||
<el-form-item> |
||||
<el-button type="primary" size="mini" :loading="selfAuditing" @click="handleSelfAudit">检测</el-button> |
||||
</el-form-item> |
||||
</el-form> |
||||
<div v-if="selfAuditResult" class="panel-note">检测结果:{{ selfAuditResult }}</div> |
||||
</div> |
||||
|
||||
<div class="panel"> |
||||
<div class="panel-title">预警列表</div> |
||||
<el-alert |
||||
title="预警列表与处置(放行 / 冻结 24h / 黑名单)后端暂未提供接口,待补齐后再接入。" |
||||
type="warning" |
||||
:closable="false" |
||||
show-icon |
||||
/> |
||||
<div class="panel-note">防刷 4 维度开关已在「安全兑换管理」页维护,此处不重复。</div> |
||||
</div> |
||||
</el-tab-pane> |
||||
|
||||
<!-- 审计日志 --> |
||||
<el-tab-pane label="审计日志" name="log"> |
||||
<data-list-panel modern-table> |
||||
<template #filters> |
||||
<el-form :inline="true" size="small" class="data-list-filter-form"> |
||||
<el-form-item label="操作类型"> |
||||
<el-select v-model="queryParams.operationType" clearable placeholder="全部类型"> |
||||
<el-option v-for="item in operationTypeOptions" :key="item.value" :label="item.label" :value="item.value" /> |
||||
</el-select> |
||||
</el-form-item> |
||||
<el-form-item label="操作人"> |
||||
<el-input v-model="queryParams.operator" clearable placeholder="操作人" @keyup.enter.native="handleQuery" /> |
||||
</el-form-item> |
||||
<el-form-item label="业务单号"> |
||||
<el-input v-model="queryParams.bizId" clearable placeholder="业务单号" @keyup.enter.native="handleQuery" /> |
||||
</el-form-item> |
||||
<el-form-item> |
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button> |
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button> |
||||
</el-form-item> |
||||
</el-form> |
||||
</template> |
||||
|
||||
<template #actions> |
||||
<el-button icon="el-icon-refresh" size="mini" @click="getLogs">刷新</el-button> |
||||
</template> |
||||
|
||||
<el-table v-loading="logLoading" :data="logList" size="mini" class="data-list-table"> |
||||
<el-table-column type="expand"> |
||||
<template slot-scope="scope"> |
||||
<div class="log-detail"> |
||||
<div><b>操作前:</b><pre>{{ formatJson(scope.row.beforeData) }}</pre></div> |
||||
<div><b>操作后:</b><pre>{{ formatJson(scope.row.afterData) }}</pre></div> |
||||
</div> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column label="操作时间" width="170"> |
||||
<template slot-scope="scope">{{ formatTime(scope.row.createTime) }}</template> |
||||
</el-table-column> |
||||
<el-table-column label="操作人" prop="operator" width="120" show-overflow-tooltip> |
||||
<template slot-scope="scope">{{ scope.row.operator || '—' }}</template> |
||||
</el-table-column> |
||||
<el-table-column label="操作 IP" prop="operatorIp" width="140" show-overflow-tooltip> |
||||
<template slot-scope="scope">{{ scope.row.operatorIp || '—' }}</template> |
||||
</el-table-column> |
||||
<el-table-column label="操作类型" width="130" align="center"> |
||||
<template slot-scope="scope"> |
||||
<el-tag :type="operationTypeTag(scope.row)" size="small">{{ operationTypeLabel(scope.row) }}</el-tag> |
||||
</template> |
||||
</el-table-column> |
||||
<el-table-column label="业务单号" prop="bizId" min-width="150" show-overflow-tooltip> |
||||
<template slot-scope="scope">{{ scope.row.bizId || '—' }}</template> |
||||
</el-table-column> |
||||
<el-table-column label="备注" prop="remark" min-width="180" show-overflow-tooltip> |
||||
<template slot-scope="scope">{{ scope.row.remark || '—' }}</template> |
||||
</el-table-column> |
||||
</el-table> |
||||
|
||||
<template #footer> |
||||
<el-pagination |
||||
background |
||||
layout="total, sizes, prev, pager, next, jumper" |
||||
:total="logTotal" |
||||
:current-page.sync="queryParams.pageNum" |
||||
:page-size.sync="queryParams.pageSize" |
||||
:page-sizes="[10, 20, 50]" |
||||
@size-change="getLogs" |
||||
@current-change="getLogs" |
||||
/> |
||||
</template> |
||||
</data-list-panel> |
||||
</el-tab-pane> |
||||
</el-tabs> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import SelectTree from '@/components/SelectTree' |
||||
import { listAuditLogs, detectAbnormal, detectSelfAudit } from '@/api/risk' |
||||
|
||||
const OPERATION_TYPES = [ |
||||
{ value: 'AUDIT_PASS', label: '审核通过' }, |
||||
{ value: 'POINTS_ADJUST', label: '积分调整' }, |
||||
{ value: 'EXCHANGE_REFUND', label: '兑换退款' } |
||||
] |
||||
|
||||
export default { |
||||
name: 'RiskAudit', |
||||
components: { SelectTree }, |
||||
data() { |
||||
return { |
||||
activeTab: 'alert', |
||||
detectForm: { userId: '' }, |
||||
selfAuditForm: { reporterId: '', auditorId: '' }, |
||||
detecting: false, |
||||
selfAuditing: false, |
||||
detectResult: null, |
||||
selfAuditResult: '', |
||||
operationTypeOptions: OPERATION_TYPES, |
||||
logLoading: false, |
||||
logList: [], |
||||
logTotal: 0, |
||||
queryParams: { pageNum: 1, pageSize: 10, operationType: undefined, operator: undefined, bizId: undefined } |
||||
} |
||||
}, |
||||
computed: { |
||||
detectChecks() { |
||||
const result = this.detectResult || {} |
||||
return [ |
||||
{ key: 'report', name: '上报防刷', ...(result.reportCheck || {}) }, |
||||
{ key: 'points', name: '积分获取防刷', ...(result.pointsEarnCheck || {}) }, |
||||
{ key: 'exchange', name: '兑换防刷', ...(result.exchangeCheck || {}) } |
||||
] |
||||
} |
||||
}, |
||||
created() { |
||||
this.getLogs() |
||||
}, |
||||
methods: { |
||||
handleDetectUserSelect(node) { |
||||
this.$set(this.detectForm, 'userId', node.id) |
||||
}, |
||||
handleSelfAuditSelect(field, node) { |
||||
this.$set(this.selfAuditForm, field, node.id) |
||||
}, |
||||
handleDetect() { |
||||
if (!this.detectForm.userId) { |
||||
this.$modal.msgWarning('请先选择要检测的用户') |
||||
return |
||||
} |
||||
this.detecting = true |
||||
detectAbnormal(this.detectForm.userId) |
||||
.then(response => { |
||||
this.detectResult = response.data || response || {} |
||||
}) |
||||
.catch(() => { |
||||
this.detectResult = null |
||||
this.$modal.msgError('异常行为检测失败,请稍后重试') |
||||
}) |
||||
.finally(() => { |
||||
this.detecting = false |
||||
}) |
||||
}, |
||||
handleSelfAudit() { |
||||
if (!this.selfAuditForm.reporterId || !this.selfAuditForm.auditorId) { |
||||
this.$modal.msgWarning('请选择上报人和审核人') |
||||
return |
||||
} |
||||
this.selfAuditing = true |
||||
detectSelfAudit(this.selfAuditForm.reporterId, this.selfAuditForm.auditorId) |
||||
.then(response => { |
||||
this.selfAuditResult = response.data || response || '—' |
||||
}) |
||||
.catch(() => { |
||||
this.selfAuditResult = '' |
||||
this.$modal.msgError('自报自审检测失败,请稍后重试') |
||||
}) |
||||
.finally(() => { |
||||
this.selfAuditing = false |
||||
}) |
||||
}, |
||||
getLogs() { |
||||
this.logLoading = true |
||||
listAuditLogs(this.queryParams) |
||||
.then(response => { |
||||
const page = response.data || response |
||||
this.logList = page.list || page.rows || page.records || [] |
||||
this.logTotal = Number(page.total || page.totalCount || this.logList.length) |
||||
}) |
||||
.catch(() => { |
||||
this.logList = [] |
||||
this.logTotal = 0 |
||||
this.$modal.msgError('审计日志加载失败,请检查后端服务和登录状态') |
||||
}) |
||||
.finally(() => { |
||||
this.logLoading = false |
||||
}) |
||||
}, |
||||
handleQuery() { |
||||
this.queryParams.pageNum = 1 |
||||
this.getLogs() |
||||
}, |
||||
resetQuery() { |
||||
this.queryParams = { pageNum: 1, pageSize: this.queryParams.pageSize, operationType: undefined, operator: undefined, bizId: undefined } |
||||
this.getLogs() |
||||
}, |
||||
operationTypeLabel(row) { |
||||
const item = OPERATION_TYPES.find(option => option.value === row.operationType) |
||||
return item ? item.label : (row.operationType || '—') |
||||
}, |
||||
operationTypeTag(row) { |
||||
const map = { AUDIT_PASS: 'primary', POINTS_ADJUST: 'warning', EXCHANGE_REFUND: 'success' } |
||||
return map[row.operationType] || 'info' |
||||
}, |
||||
formatJson(value) { |
||||
if (!value) return '—' |
||||
try { |
||||
return JSON.stringify(JSON.parse(value), null, 2) |
||||
} catch (error) { |
||||
return String(value) |
||||
} |
||||
}, |
||||
formatTime(value) { |
||||
if (!value) return '—' |
||||
return String(value).replace('T', ' ').slice(0, 19) |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" scoped> |
||||
.risk-audit { |
||||
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-title { |
||||
margin-bottom: 12px; |
||||
color: var(--prototype-text, #1a1a2e); |
||||
font-size: 14px; |
||||
font-weight: 600; |
||||
} |
||||
|
||||
.panel-note { |
||||
margin-top: 10px; |
||||
color: var(--prototype-text-muted, #9898ae); |
||||
font-size: 12px; |
||||
line-height: 20px; |
||||
} |
||||
|
||||
.detect-result { |
||||
margin-top: 12px; |
||||
padding: 12px 14px; |
||||
border-radius: 8px; |
||||
background: var(--prototype-panel-soft, #fafbfc); |
||||
} |
||||
|
||||
.detect-result__overall { |
||||
margin-bottom: 10px; |
||||
font-size: 13px; |
||||
font-weight: 600; |
||||
} |
||||
|
||||
.detect-result__overall.is-pass { |
||||
color: #2d8a4e; |
||||
} |
||||
|
||||
.detect-result__overall.is-fail { |
||||
color: #d64550; |
||||
} |
||||
|
||||
.detect-result__item { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 10px; |
||||
padding: 4px 0; |
||||
font-size: 13px; |
||||
} |
||||
|
||||
.detect-result__name { |
||||
width: 110px; |
||||
color: var(--prototype-text-secondary, #5a5a72); |
||||
} |
||||
|
||||
.detect-result__reason { |
||||
color: var(--prototype-text-muted, #9898ae); |
||||
} |
||||
|
||||
.log-detail { |
||||
padding: 8px 16px; |
||||
} |
||||
|
||||
.log-detail pre { |
||||
margin: 4px 0 12px; |
||||
padding: 8px 10px; |
||||
max-height: 220px; |
||||
overflow: auto; |
||||
background: #f7f8fa; |
||||
border-radius: 6px; |
||||
color: var(--prototype-text, #1a1a2e); |
||||
font-size: 12px; |
||||
white-space: pre-wrap; |
||||
word-break: break-all; |
||||
} |
||||
</style> |
||||
Loading…
Reference in new issue