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 用例全绿;线上实测告警接口/审计落库/备份均通过
这个提交包含在:
xcbyy
2026-10-22 09:54:10 +08:00
父节点 d941a16d28
当前提交 65ddc7301a
共有 10 个文件被更改,包括 562 次插入 和 0 次删除

查看文件

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