feat(esb): T8.3 编码映射+监控+可靠性 - AppService/Controller/Frontend
This commit is contained in:
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
5
healthlink-his-ui/src/views/esbmanage/esbmonitor/api.js
Normal file
5
healthlink-his-ui/src/views/esbmanage/esbmonitor/api.js
Normal 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' }) }
|
||||
154
healthlink-his-ui/src/views/esbmanage/esbmonitor/index.vue
Normal file
154
healthlink-his-ui/src/views/esbmanage/esbmonitor/index.vue
Normal 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>
|
||||
Reference in New Issue
Block a user