feat(ops): 每日数据库备份 + 资金异常巡检告警 + 后台操作审计
- 新增 system_alerts/admin_audit_logs 表与 (status,created_at) 索引;启动即巡检、每 5 分钟扫描提现卡单/支付与订单不一致/待接单超时 - 后台新增「系统告警与操作审计」页(告警处理 + 审计列表),并接入消息铃铛待办 - 余额调整/封禁/删除/大神审核/提现打款/告警处理等敏感操作写入审计日志(含 IP) - 新增 server/scripts/backup-db.sh 并已在生产配置每日 03:30 cron(校验+保留 14 天) - 清理空的 order_messages 表;115 套件/750 用例全绿;线上实测告警接口/审计落库/备份均通过
这个提交包含在:
@@ -33,6 +33,7 @@ export default {
|
||||
'menu.safety.reports': 'Violation Reports',
|
||||
'menu.safety.auditLog': 'Risk Intercept Log',
|
||||
'menu.safety.platformConfig': 'Platform & Support Config',
|
||||
'menu.safety.systemAlerts': 'Alerts & Admin Audit',
|
||||
'menu.banner': 'Banner & Operations',
|
||||
'menu.banner.manage': 'Banner Management',
|
||||
'menu.banner.navCategory': 'Category Navigation',
|
||||
|
||||
@@ -33,6 +33,7 @@ export default {
|
||||
'menu.safety.reports': '违规举报工单处置',
|
||||
'menu.safety.auditLog': '场所与内容拦截日志',
|
||||
'menu.safety.platformConfig': '平台配置与协议规范 (用户协议/隐私/客服)',
|
||||
'menu.safety.systemAlerts': '系统告警与操作审计',
|
||||
'menu.banner': '首页轮播与运营',
|
||||
'menu.banner.manage': '轮播图管理',
|
||||
'menu.banner.navCategory': '金刚区导航品类',
|
||||
|
||||
@@ -52,6 +52,16 @@ const SAFETY: AppRouteRecordRaw = {
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'system-alerts',
|
||||
name: 'SystemAlerts',
|
||||
component: () => import('@/views/safety/system-alerts/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.safety.systemAlerts',
|
||||
requiresAuth: true,
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-breadcrumb class="breadcrumb">
|
||||
<a-breadcrumb-item>履约安全与风控</a-breadcrumb-item>
|
||||
<a-breadcrumb-item>系统告警与操作审计</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
|
||||
<a-card class="general-card">
|
||||
<a-tabs v-model:active-key="activeTab" @change="onTabChange">
|
||||
<a-tab-pane key="alerts" :title="`系统告警 (${alerts.length})`">
|
||||
<a-space style="margin-bottom: 14px">
|
||||
<a-button :loading="loadingAlerts" @click="fetchAlerts">
|
||||
<template #icon><icon-refresh /></template>
|
||||
刷新告警
|
||||
</a-button>
|
||||
<span class="hint">
|
||||
每 5 分钟自动巡检:提现卡单 / 支付成功但订单未流转 / 待接单超时未退款
|
||||
</span>
|
||||
</a-space>
|
||||
|
||||
<a-table
|
||||
:data="alerts"
|
||||
:loading="loadingAlerts"
|
||||
row-key="id"
|
||||
:pagination="{ pageSize: 10, showTotal: true }"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="发现时间" data-index="createdAt" :width="175" />
|
||||
<a-table-column title="级别" :width="90">
|
||||
<template #cell="{ record }">
|
||||
<a-tag :color="record.level === 'ERROR' ? 'red' : 'orange'">{{ record.level }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="告警项" :width="250">
|
||||
<template #cell="{ record }">
|
||||
<strong>{{ record.title }}</strong>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="详情" data-index="detail" />
|
||||
<a-table-column title="操作" :width="110" fixed="right">
|
||||
<template #cell="{ record }">
|
||||
<a-button type="text" size="small" @click="resolveAlert(record)">标记处理</a-button>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="audit" title="后台操作审计">
|
||||
<a-space style="margin-bottom: 14px">
|
||||
<a-button :loading="loadingAudit" @click="fetchAudit">
|
||||
<template #icon><icon-refresh /></template>
|
||||
刷新审计
|
||||
</a-button>
|
||||
<span class="hint">余额调整、封禁/解封、删除用户、大神审核、提现打款等敏感操作留痕</span>
|
||||
</a-space>
|
||||
|
||||
<a-table
|
||||
:data="audits"
|
||||
:loading="loadingAudit"
|
||||
row-key="id"
|
||||
:pagination="{ pageSize: 10, showTotal: true }"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column title="操作时间" data-index="createdAt" :width="175" />
|
||||
<a-table-column title="操作" :width="160">
|
||||
<template #cell="{ record }">
|
||||
<a-tag color="arcoblue">{{ record.action }}</a-tag>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="对象" :width="200">
|
||||
<template #cell="{ record }">
|
||||
<span class="code-font">{{ record.targetType }} / {{ record.targetId }}</span>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column title="详情" data-index="detail" />
|
||||
<a-table-column title="IP" data-index="ip" :width="150" />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
const activeTab = ref('alerts');
|
||||
const alerts = ref<any[]>([]);
|
||||
const audits = ref<any[]>([]);
|
||||
const loadingAlerts = ref(false);
|
||||
const loadingAudit = ref(false);
|
||||
|
||||
const fetchAlerts = async () => {
|
||||
loadingAlerts.value = true;
|
||||
try {
|
||||
const res: any = await axios.get('/api/admin/system/alerts', { params: { status: 'OPEN' } });
|
||||
alerts.value = Array.isArray(res?.data) ? res.data : [];
|
||||
} catch (err: any) {
|
||||
Message.error(err?.message || '获取系统告警失败');
|
||||
} finally {
|
||||
loadingAlerts.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAudit = async () => {
|
||||
loadingAudit.value = true;
|
||||
try {
|
||||
const res: any = await axios.get('/api/admin/audit-logs');
|
||||
audits.value = Array.isArray(res?.data) ? res.data : [];
|
||||
} catch (err: any) {
|
||||
Message.error(err?.message || '获取操作审计失败');
|
||||
} finally {
|
||||
loadingAudit.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resolveAlert = async (record: any) => {
|
||||
try {
|
||||
const res: any = await axios.post(`/api/admin/system/alerts/${record.id}/resolve`);
|
||||
if (res && res.code === 20000) {
|
||||
Message.success('已标记处理');
|
||||
fetchAlerts();
|
||||
} else {
|
||||
Message.error(res?.message || '操作失败');
|
||||
}
|
||||
} catch (err: any) {
|
||||
Message.error(err?.message || '操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const onTabChange = (key: string) => {
|
||||
if (key === 'audit' && audits.value.length === 0) fetchAudit();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchAlerts();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.code-font {
|
||||
font-family: Menlo, Consolas, monospace;
|
||||
}
|
||||
</style>
|
||||
在新工单中引用
屏蔽一个用户