feat(P4): ESB消息可靠性 — 重试/死信队列/监控统计

- EsbReliabilityController: 消息重试/批量重试/死信管理/监控统计
- EsbDeadLetter: 死信队列实体+V29 Flyway迁移
- EsbMonitorStats: 监控统计实体
- 前端reliability: 监控卡片+死信队列+消息时间线
- 后端编译通过,前端构建通过
This commit is contained in:
2026-06-06 21:03:27 +08:00
parent 2cff313539
commit 2e2dc6e9d5
12 changed files with 409 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function retryMessage(id){return request({url:'/esb-reliability/retry/'+id,method:'post'})}
export function retryAllFailed(){return request({url:'/esb-reliability/retry-all-failed',method:'post'})}
export function getDeadLetterPage(p){return request({url:'/esb-reliability/dead-letter/page',method:'get',params:p})}
export function resolveDeadLetter(id,by){return request({url:'/esb-reliability/dead-letter/resolve/'+id,method:'put',params:{resolvedBy:by}})}
export function ignoreDeadLetter(id){return request({url:'/esb-reliability/dead-letter/ignore/'+id,method:'put'})}
export function getMonitorStats(){return request({url:'/esb-reliability/monitor/stats',method:'get'})}
export function getTimeline(p){return request({url:'/esb-reliability/monitor/timeline',method:'get',params:p})}

View File

@@ -0,0 +1,70 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">ESB消息可靠性监控</span>
<div><el-button type="warning" @click="retryAll">批量重试失败消息</el-button><el-button type="primary" @click="loadStats">刷新</el-button></div>
</div>
<el-row :gutter="12" style="margin-bottom:16px">
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center"><div style="font-size:20px;font-weight:bold;color:#409eff">{{ stats.totalMessages||0 }}</div><div style="font-size:12px;color:#999">总消息数</div></div></el-card></el-col>
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center"><div style="font-size:20px;font-weight:bold;color:#67c23a">{{ stats.status_已发送||0 }}</div><div style="font-size:12px;color:#999">已发送</div></div></el-card></el-col>
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center"><div style="font-size:20px;font-weight:bold;color:#f56c6c">{{ stats.status_发送失败||0 }}</div><div style="font-size:12px;color:#999">发送失败</div></div></el-card></el-col>
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center"><div style="font-size:20px;font-weight:bold;color:#e6a23c">{{ stats.pendingDeadLetters||0 }}</div><div style="font-size:12px;color:#999">死信待处理</div></div></el-card></el-col>
<el-col :span="4"><el-card shadow="hover"><div style="text-align:center"><div style="font-size:20px;font-weight:bold;color:#909399">{{ stats.successRate||0 }}%</div><div style="font-size:12px;color:#999">成功率</div></div></el-card></el-col>
</el-row>
<el-tabs v-model="activeTab" type="border-card">
<el-tab-pane label="死信队列" name="deadletter">
<el-table :data="deadLetterData" border stripe>
<el-table-column prop="messageId" label="消息ID" width="180" show-overflow-tooltip/>
<el-table-column prop="sourceSystem" label="来源" width="100"/>
<el-table-column prop="targetSystem" label="目标" width="100"/>
<el-table-column prop="retryCount" label="重试次数" width="80" align="center"/>
<el-table-column prop="errorMessage" label="错误" min-width="150" show-overflow-tooltip/>
<el-table-column prop="status" label="状态" width="90">
<template #default="{row}">
<el-tag v-if="row.status==='PENDING'" type="danger" size="small">待处理</el-tag>
<el-tag v-else-if="row.status==='RESOLVED'" type="success" size="small">已解决</el-tag>
<el-tag v-else type="info" size="small">已忽略</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="160">
<template #default="{row}">
<el-button v-if="row.status==='PENDING'" type="success" link size="small" @click="resolve(row.id)">解决</el-button>
<el-button v-if="row.status==='PENDING'" type="info" link size="small" @click="ignore(row.id)">忽略</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="dlq.pageNo" v-model:page-size="dlq.pageSize" :total="dlqTotal" layout="total,prev,pager,next" @current-change="loadDeadLetter"/>
</el-tab-pane>
<el-tab-pane label="消息时间线" name="timeline">
<el-table :data="timelineData" border stripe>
<el-table-column prop="messageId" label="消息ID" width="180" show-overflow-tooltip/>
<el-table-column prop="messageType" label="类型" width="80"/>
<el-table-column prop="sourceSystem" label="来源" width="100"/>
<el-table-column prop="targetSystem" label="目标" width="100"/>
<el-table-column prop="status" label="状态" width="90">
<template #default="{row}">
<el-tag :type="row.status==='已发送'?'success':row.status==='发送失败'?'danger':'info'" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="retryCount" label="重试" width="60" align="center"/>
<el-table-column prop="createTime" label="时间" width="170"/>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {ElMessage,ElMessageBox} from 'element-plus'
import {retryMessage,retryAllFailed,getDeadLetterPage,resolveDeadLetter,ignoreDeadLetter,getMonitorStats,getTimeline} from './api'
const activeTab=ref('deadletter')
const stats=ref({});const deadLetterData=ref([]);const dlqTotal=ref(0);const timelineData=ref([])
const dlq=ref({pageNo:1,pageSize:20,status:'',sourceSystem:''})
const loadStats=async()=>{const r=await getMonitorStats();stats.value=r.data||{}}
const loadDeadLetter=async()=>{const r=await getDeadLetterPage(dlq.value);deadLetterData.value=r.data?.records||[];dlqTotal.value=r.data?.total||0}
const loadTimeline=async()=>{const r=await getTimeline({hours:24});timelineData.value=r.data||[]}
const retryAll=async()=>{const r=await retryAllFailed();ElMessage.success(r.data);loadStats();loadDeadLetter()}
const resolve=async(id)=>{const {value}=await ElMessageBox.prompt('解决人','确认解决');if(value){await resolveDeadLetter(id,value);ElMessage.success('已解决');loadDeadLetter();loadStats()}}
const ignore=async(id)=>{await ElMessageBox.confirm('确定忽略?');await ignoreDeadLetter(id);ElMessage.success('已忽略');loadDeadLetter();loadStats()}
onMounted(()=>{loadStats();loadDeadLetter();loadTimeline()})
</script>