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