feat(esb): T8.3 编码映射+监控+可靠性 - AppService/Controller/Frontend

This commit is contained in:
2026-06-18 12:58:36 +08:00
parent 2d67395228
commit 20934572d2
5 changed files with 343 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
package com.healthlink.his.web.esbmanage.appservice;
import com.healthlink.his.esb.domain.CodeMapping;
import com.healthlink.his.esb.domain.EsbDeadLetter;
import com.healthlink.his.esb.domain.EsbMonitorStats;
import java.util.List;
import java.util.Map;
public interface IEsbMonitorAppService {
Map<String, Object> getMonitorStats();
List<EsbDeadLetter> getDeadLetters(String status, String sourceSystem);
List<CodeMapping> getCodeMappings(String mappingType, String sourceSystem);
Map<String, Object> getCodeMappingStats();
}

View File

@@ -0,0 +1,114 @@
package com.healthlink.his.web.esbmanage.appservice.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.healthlink.his.esb.domain.*;
import com.healthlink.his.esb.service.*;
import com.healthlink.his.web.esbmanage.appservice.IEsbMonitorAppService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class EsbMonitorAppServiceImpl implements IEsbMonitorAppService {
private final IEsbMessageService messageService;
private final IEsbDeadLetterService deadLetterService;
private final IEsbMonitorStatsService monitorStatsService;
private final ICodeMappingService codeMappingService;
private final IEsbServiceRegistryService registryService;
@Override
public Map<String, Object> getMonitorStats() {
Map<String, Object> stats = new LinkedHashMap<>();
long totalMessages = messageService.count();
stats.put("totalMessages", totalMessages);
String[] statuses = {"待发送", "已发送", "发送失败", "重试中", "死信"};
Map<String, Long> statusCounts = new LinkedHashMap<>();
for (String s : statuses) {
long count = messageService.count(new LambdaQueryWrapper<EsbMessage>().eq(EsbMessage::getStatus, s));
statusCounts.put(s, count);
}
stats.put("statusCounts", statusCounts);
long successCount = statusCounts.getOrDefault("已发送", 0L);
stats.put("successRate", totalMessages > 0 ? Math.round(successCount * 100.0 / totalMessages) : 100);
long pendingDeadLetters = deadLetterService.count(
new LambdaQueryWrapper<EsbDeadLetter>().eq(EsbDeadLetter::getStatus, "PENDING"));
stats.put("pendingDeadLetters", pendingDeadLetters);
long totalDeadLetters = deadLetterService.count();
stats.put("totalDeadLetters", totalDeadLetters);
long totalMappings = codeMappingService.count();
stats.put("totalCodeMappings", totalMappings);
long enabledServices = registryService.count(
new LambdaQueryWrapper<EsbServiceRegistry>().eq(EsbServiceRegistry::getServiceStatus, "启用"));
long totalServices = registryService.count();
stats.put("enabledServices", enabledServices);
stats.put("totalServices", totalServices);
LambdaQueryWrapper<EsbMonitorStats> statsWrapper = new LambdaQueryWrapper<>();
statsWrapper.orderByDesc(EsbMonitorStats::getStatHour).last("LIMIT 24");
List<EsbMonitorStats> recentStats = monitorStatsService.list(statsWrapper);
int totalRetry = recentStats.stream().mapToInt(s -> s.getRetryCount() != null ? s.getRetryCount() : 0).sum();
int totalFail = recentStats.stream().mapToInt(s -> s.getFailCount() != null ? s.getFailCount() : 0).sum();
int totalSuccess = recentStats.stream().mapToInt(s -> s.getSuccessCount() != null ? s.getSuccessCount() : 0).sum();
double avgDuration = recentStats.stream()
.filter(s -> s.getAvgDurationMs() != null)
.mapToInt(EsbMonitorStats::getAvgDurationMs)
.average().orElse(0.0);
stats.put("recentTotal", totalRetry + totalFail + totalSuccess);
stats.put("recentRetry", totalRetry);
stats.put("recentFail", totalFail);
stats.put("recentSuccess", totalSuccess);
stats.put("avgDurationMs", Math.round(avgDuration));
return stats;
}
@Override
public List<EsbDeadLetter> getDeadLetters(String status, String sourceSystem) {
LambdaQueryWrapper<EsbDeadLetter> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(status), EsbDeadLetter::getStatus, status)
.like(StringUtils.hasText(sourceSystem), EsbDeadLetter::getSourceSystem, sourceSystem)
.orderByDesc(EsbDeadLetter::getCreateTime);
return deadLetterService.list(wrapper);
}
@Override
public List<CodeMapping> getCodeMappings(String mappingType, String sourceSystem) {
LambdaQueryWrapper<CodeMapping> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.hasText(mappingType), CodeMapping::getMappingType, mappingType)
.eq(StringUtils.hasText(sourceSystem), CodeMapping::getSourceSystem, sourceSystem)
.orderByDesc(CodeMapping::getCreateTime);
return codeMappingService.list(wrapper);
}
@Override
public Map<String, Object> getCodeMappingStats() {
Map<String, Object> stats = new LinkedHashMap<>();
long total = codeMappingService.count();
stats.put("total", total);
List<CodeMapping> allMappings = codeMappingService.list();
Map<String, Long> byType = allMappings.stream()
.collect(Collectors.groupingBy(CodeMapping::getMappingType, Collectors.counting()));
stats.put("byType", byType);
Map<String, Long> bySource = allMappings.stream()
.collect(Collectors.groupingBy(CodeMapping::getSourceSystem, Collectors.counting()));
stats.put("bySource", bySource);
return stats;
}
}

View File

@@ -0,0 +1,55 @@
package com.healthlink.his.web.esbmanage.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.esb.domain.CodeMapping;
import com.healthlink.his.esb.domain.EsbDeadLetter;
import com.healthlink.his.web.esbmanage.appservice.IEsbMonitorAppService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* ESB监控+编码映射 Controller — 统计/死信/编码映射查询
*/
@RestController
@RequestMapping("/esb/monitor")
@Slf4j
@RequiredArgsConstructor
public class EsbMonitorController {
private final IEsbMonitorAppService esbMonitorAppService;
@GetMapping("/stats")
@PreAuthorize("hasAuthority('infection:esb:list')")
public R<?> getMonitorStats() {
return R.ok(esbMonitorAppService.getMonitorStats());
}
@GetMapping("/dead-letters")
@PreAuthorize("hasAuthority('infection:esb:list')")
public R<?> getDeadLetters(
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "sourceSystem", required = false) String sourceSystem) {
List<EsbDeadLetter> list = esbMonitorAppService.getDeadLetters(status, sourceSystem);
return R.ok(list);
}
@GetMapping("/mapping/list")
@PreAuthorize("hasAuthority('infection:esb:list')")
public R<?> getCodeMappings(
@RequestParam(value = "mappingType", required = false) String mappingType,
@RequestParam(value = "sourceSystem", required = false) String sourceSystem) {
List<CodeMapping> list = esbMonitorAppService.getCodeMappings(mappingType, sourceSystem);
return R.ok(list);
}
@GetMapping("/mapping/stats")
@PreAuthorize("hasAuthority('infection:esb:list')")
public R<?> getCodeMappingStats() {
return R.ok(esbMonitorAppService.getCodeMappingStats());
}
}

View File

@@ -0,0 +1,5 @@
import request from '@/utils/request'
export function getMonitorStats() { return request({ url: '/esb/monitor/stats', method: 'get' }) }
export function getDeadLetters(params) { return request({ url: '/esb/monitor/dead-letters', method: 'get', params }) }
export function getCodeMappings(params) { return request({ url: '/esb/monitor/mapping/list', method: 'get', params }) }
export function getCodeMappingStats() { return request({ url: '/esb/monitor/mapping/stats', method: 'get' }) }

View File

@@ -0,0 +1,154 @@
<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>
<el-button type="primary" @click="loadAll">刷新</el-button>
</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.successRate || 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.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:#e6a23c">{{ stats.totalCodeMappings || 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.enabledServices || 0 }}/{{ stats.totalServices || 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.avgDurationMs || 0 }}ms</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="deadLetterList" 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="messageType" label="类型" width="100" />
<el-table-column prop="retryCount" label="重试次数" width="80" align="center" />
<el-table-column prop="errorMessage" label="错误" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="90">
<template #default="{row}">
<el-tag :type="row.status==='PENDING'?'danger':row.status==='RESOLVED'?'success':'info'" size="small">
{{ row.status === 'PENDING' ? '待处理' : row.status === 'RESOLVED' ? '已解决' : '已忽略' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="时间" width="170" />
</el-table>
</el-tab-pane>
<el-tab-pane label="编码映射" name="mapping">
<div style="margin-bottom:12px;display:flex;gap:12px">
<el-select v-model="mappingFilter.mappingType" clearable placeholder="映射类型" style="width:160px">
<el-option label="诊断" value="diagnosis" />
<el-option label="手术" value="procedure" />
<el-option label="药品" value="medication" />
<el-option label="观察" value="observation" />
</el-select>
<el-select v-model="mappingFilter.sourceSystem" clearable placeholder="来源系统" style="width:160px">
<el-option label="HIS" value="HIS" />
<el-option label="LIS" value="LIS" />
<el-option label="PACS" value="PACS" />
<el-option label="EMR" value="EMR" />
</el-select>
<el-button type="primary" @click="loadMappings">查询</el-button>
</div>
<el-table :data="mappingList" border stripe>
<el-table-column prop="sourceSystem" label="来源系统" width="100" />
<el-table-column prop="sourceCode" label="来源编码" width="120" />
<el-table-column prop="targetSystem" label="目标系统" width="100" />
<el-table-column prop="targetCode" label="目标编码" width="120" />
<el-table-column prop="mappingType" label="映射类型" width="100">
<template #default="{row}">
<el-tag size="small">{{ typeMap[row.mappingType] || row.mappingType }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<el-tab-pane label="监控统计" name="stats">
<el-descriptions :column="2" border>
<el-descriptions-item label="总消息数">{{ stats.totalMessages || 0 }}</el-descriptions-item>
<el-descriptions-item label="成功率">{{ stats.successRate || 0 }}%</el-descriptions-item>
<el-descriptions-item label="死信总数">{{ stats.totalDeadLetters || 0 }}</el-descriptions-item>
<el-descriptions-item label="死信待处理">{{ stats.pendingDeadLetters || 0 }}</el-descriptions-item>
<el-descriptions-item label="编码映射数">{{ stats.totalCodeMappings || 0 }}</el-descriptions-item>
<el-descriptions-item label="启用服务数">{{ stats.enabledServices || 0 }}/{{ stats.totalServices || 0 }}</el-descriptions-item>
<el-descriptions-item label="近期消息总量">{{ stats.recentTotal || 0 }}</el-descriptions-item>
<el-descriptions-item label="近期成功">{{ stats.recentSuccess || 0 }}</el-descriptions-item>
<el-descriptions-item label="近期失败">{{ stats.recentFail || 0 }}</el-descriptions-item>
<el-descriptions-item label="近期重试">{{ stats.recentRetry || 0 }}</el-descriptions-item>
<el-descriptions-item label="平均延迟">{{ stats.avgDurationMs || 0 }}ms</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { getMonitorStats, getDeadLetters, getCodeMappings } from './api'
const typeMap = { diagnosis: '诊断', procedure: '手术', medication: '药品', observation: '观察' }
const activeTab = ref('deadletter')
const stats = ref({})
const deadLetterList = ref([])
const mappingList = ref([])
const mappingFilter = ref({ mappingType: '', sourceSystem: '' })
const loadStats = async () => {
const r = await getMonitorStats()
stats.value = r.data || {}
}
const loadDeadLetters = async () => {
const r = await getDeadLetters({})
deadLetterList.value = r.data || []
}
const loadMappings = async () => {
const r = await getCodeMappings(mappingFilter.value)
mappingList.value = r.data || []
}
const loadAll = async () => {
await Promise.all([loadStats(), loadDeadLetters(), loadMappings()])
}
onMounted(() => { loadAll() })
</script>