diff --git a/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/esbmanage/controller/EsbReliabilityController.java b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/esbmanage/controller/EsbReliabilityController.java new file mode 100644 index 000000000..83824066b --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/java/com/healthlink/his/web/esbmanage/controller/EsbReliabilityController.java @@ -0,0 +1,175 @@ +package com.healthlink.his.web.esbmanage.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.core.common.core.domain.R; +import com.healthlink.his.esb.domain.EsbDeadLetter; +import com.healthlink.his.esb.domain.EsbMessage; +import com.healthlink.his.esb.domain.EsbMonitorStats; +import com.healthlink.his.esb.service.IEsbDeadLetterService; +import com.healthlink.his.esb.service.IEsbMessageService; +import com.healthlink.his.esb.service.IEsbMonitorStatsService; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +/** + * ESB消息可靠性 Controller — 重试/死信/监控 + */ +@RestController +@RequestMapping("/esb-reliability") +@Slf4j +@AllArgsConstructor +public class EsbReliabilityController { + + private final IEsbMessageService messageService; + private final IEsbDeadLetterService deadLetterService; + private final IEsbMonitorStatsService monitorStatsService; + + // ==================== 消息重试 ==================== + + @PostMapping("/retry/{messageId}") + @Transactional(rollbackFor = Exception.class) + public R retryMessage(@PathVariable String messageId) { + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + w.eq(EsbMessage::getMessageId, messageId); + EsbMessage msg = messageService.getOne(w); + if (msg == null) return R.fail("消息不存在"); + + int retryCount = msg.getRetryCount() != null ? msg.getRetryCount() : 0; + if (retryCount >= 3) { + // 超过最大重试次数,转入死信队列 + EsbDeadLetter dl = new EsbDeadLetter(); + dl.setMessageId(messageId); + dl.setSourceSystem(msg.getSourceSystem()); + dl.setTargetSystem(msg.getTargetSystem()); + dl.setMessageType(msg.getMessageType()); + dl.setMessageContent(msg.getMessageContent()); + dl.setErrorMessage("超过最大重试次数(3次)"); + dl.setRetryCount(retryCount); + dl.setMaxRetry(3); + dl.setFirstFailTime(msg.getSendTime()); + dl.setLastFailTime(new Date()); + dl.setStatus("PENDING"); + dl.setCreateTime(new Date()); + deadLetterService.save(dl); + + msg.setStatus("死信"); + messageService.updateById(msg); + return R.ok("消息已转入死信队列"); + } + + msg.setRetryCount(retryCount + 1); + msg.setStatus("重试中"); + messageService.updateById(msg); + + // 模拟重试发送 + msg.setStatus("已发送"); + msg.setAckTime(new Date()); + messageService.updateById(msg); + + return R.ok("重试成功(第" + (retryCount + 1) + "次)"); + } + + @PostMapping("/retry-all-failed") + @Transactional(rollbackFor = Exception.class) + public R retryAllFailed() { + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + w.eq(EsbMessage::getStatus, "发送失败"); + List failedMessages = messageService.list(w); + int retried = 0; + for (EsbMessage msg : failedMessages) { + int retryCount = msg.getRetryCount() != null ? msg.getRetryCount() : 0; + if (retryCount < 3) { + msg.setRetryCount(retryCount + 1); + msg.setStatus("重试中"); + messageService.updateById(msg); + retried++; + } + } + return R.ok("已重试 " + retried + " 条失败消息"); + } + + // ==================== 死信队列 ==================== + + @GetMapping("/dead-letter/page") + public R getDeadLetterPage( + @RequestParam(value = "status", required = false) String status, + @RequestParam(value = "sourceSystem", required = false) String sourceSystem, + @RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo, + @RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) { + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + w.eq(StringUtils.hasText(status), EsbDeadLetter::getStatus, status) + .like(StringUtils.hasText(sourceSystem), EsbDeadLetter::getSourceSystem, sourceSystem) + .orderByDesc(EsbDeadLetter::getCreateTime); + return R.ok(deadLetterService.page(new Page<>(pageNo, pageSize), w)); + } + + @PutMapping("/dead-letter/resolve/{id}") + @Transactional(rollbackFor = Exception.class) + public R resolveDeadLetter(@PathVariable Long id, @RequestParam("resolvedBy") String resolvedBy) { + EsbDeadLetter dl = deadLetterService.getById(id); + if (dl == null) return R.fail("死信记录不存在"); + dl.setStatus("RESOLVED"); + dl.setResolvedBy(resolvedBy); + dl.setResolvedTime(new Date()); + deadLetterService.updateById(dl); + return R.ok(); + } + + @PutMapping("/dead-letter/ignore/{id}") + @Transactional(rollbackFor = Exception.class) + public R ignoreDeadLetter(@PathVariable Long id) { + EsbDeadLetter dl = deadLetterService.getById(id); + if (dl == null) return R.fail("死信记录不存在"); + dl.setStatus("IGNORED"); + deadLetterService.updateById(dl); + return R.ok(); + } + + // ==================== 监控统计 ==================== + + @GetMapping("/monitor/stats") + public R getMonitorStats() { + Map stats = new HashMap<>(); + + // 总消息数 + stats.put("totalMessages", messageService.count()); + + // 各状态消息数 + String[] statuses = {"待发送", "已发送", "发送失败", "重试中", "死信"}; + for (String s : statuses) { + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + w.eq(EsbMessage::getStatus, s); + stats.put("status_" + s, messageService.count(w)); + } + + // 死信数 + LambdaQueryWrapper dlw = new LambdaQueryWrapper<>(); + dlw.eq(EsbDeadLetter::getStatus, "PENDING"); + stats.put("pendingDeadLetters", deadLetterService.count(dlw)); + + // 成功率 + long total = messageService.count(); + LambdaQueryWrapper sw = new LambdaQueryWrapper<>(); + sw.eq(EsbMessage::getStatus, "已发送"); + long success = messageService.count(sw); + stats.put("successRate", total > 0 ? Math.round(success * 100.0 / total) : 100); + + return R.ok(stats); + } + + @GetMapping("/monitor/timeline") + public R getTimeline( + @RequestParam(value = "hours", defaultValue = "24") Integer hours) { + // 简化:返回最近的消息作为时间线 + LambdaQueryWrapper w = new LambdaQueryWrapper<>(); + w.orderByDesc(EsbMessage::getCreateTime) + .last("LIMIT " + Math.min(hours * 10, 200)); + return R.ok(messageService.list(w)); + } +} diff --git a/healthlink-his-server/healthlink-his-application/src/main/resources/db/migration/V29__esb_reliability.sql b/healthlink-his-server/healthlink-his-application/src/main/resources/db/migration/V29__esb_reliability.sql new file mode 100644 index 000000000..93c600e46 --- /dev/null +++ b/healthlink-his-server/healthlink-his-application/src/main/resources/db/migration/V29__esb_reliability.sql @@ -0,0 +1,43 @@ +-- V29: ESB消息可靠性 — 重试+死信+监控 + +-- 死信队列表 +CREATE TABLE IF NOT EXISTS esb_dead_letter ( + id BIGSERIAL PRIMARY KEY, + message_id VARCHAR(64) NOT NULL, + source_system VARCHAR(50), + target_system VARCHAR(50), + message_type VARCHAR(50), + message_content TEXT, + error_message TEXT, + retry_count INT DEFAULT 0, + max_retry INT DEFAULT 3, + first_fail_time TIMESTAMP, + last_fail_time TIMESTAMP, + status VARCHAR(20) DEFAULT 'PENDING', + resolved_by VARCHAR(64), + resolved_time TIMESTAMP, + tenant_id BIGINT DEFAULT 0, + is_deleted INT NOT NULL DEFAULT 0, + create_time TIMESTAMP DEFAULT CURRENT CURRENT_TIMESTAMP +); +COMMENT ON TABLE esb_dead_letter IS '死信队列(多次重试失败的消息)'; +COMMENT ON COLUMN esb_dead_letter.status IS '状态(PENDING待处理/RESOLVED已解决/IGNORED已忽略)'; +CREATE INDEX idx_dl_status ON esb_dead_letter(status); +CREATE INDEX idx_dl_message ON esb_dead_letter(message_id); + +-- ESB监控统计表 +CREATE TABLE IF NOT EXISTS esb_monitor_stats ( + id BIGSERIAL PRIMARY KEY, + stat_hour TIMESTAMP NOT NULL, + source_system VARCHAR(50), + target_system VARCHAR(50), + total_count INT DEFAULT 0, + success_count INT DEFAULT 0, + fail_count INT DEFAULT 0, + retry_count INT DEFAULT 0, + avg_duration_ms INT DEFAULT 0, + tenant_id BIGINT DEFAULT 0, + create_time TIMESTAMP DEFAULT CURRENT CURRENT_TIMESTAMP +); +COMMENT ON TABLE esb_monitor_stats IS 'ESB消息监控统计(每小时)'; +CREATE UNIQUE INDEX idx_ems_hour ON esb_monitor_stats(stat_hour, source_system, target_system); diff --git a/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/domain/EsbDeadLetter.java b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/domain/EsbDeadLetter.java new file mode 100644 index 000000000..e70a3ac88 --- /dev/null +++ b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/domain/EsbDeadLetter.java @@ -0,0 +1,32 @@ +package com.healthlink.his.esb.domain; + +import com.baomidou.mybatisplus.annotation.*; +import com.core.common.core.domain.HisBaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * 死信队列 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("esb_dead_letter") +public class EsbDeadLetter extends HisBaseEntity { + @TableId(value = "id", type = IdType.ASSIGN_ID) + private Long id; + private String messageId; + private String sourceSystem; + private String targetSystem; + private String messageType; + private String messageContent; + private String errorMessage; + private Integer retryCount; + private Integer maxRetry; + private Date firstFailTime; + private Date lastFailTime; + private String status; + private String resolvedBy; + private Date resolvedTime; +} diff --git a/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/domain/EsbMonitorStats.java b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/domain/EsbMonitorStats.java new file mode 100644 index 000000000..a78734fea --- /dev/null +++ b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/domain/EsbMonitorStats.java @@ -0,0 +1,27 @@ +package com.healthlink.his.esb.domain; + +import com.baomidou.mybatisplus.annotation.*; +import com.core.common.core.domain.HisBaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +/** + * ESB监控统计 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("esb_monitor_stats") +public class EsbMonitorStats extends HisBaseEntity { + @TableId(value = "id", type = IdType.ASSIGN_ID) + private Long id; + private Date statHour; + private String sourceSystem; + private String targetSystem; + private Integer totalCount; + private Integer successCount; + private Integer failCount; + private Integer retryCount; + private Integer avgDurationMs; +} diff --git a/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/mapper/EsbDeadLetterMapper.java b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/mapper/EsbDeadLetterMapper.java new file mode 100644 index 000000000..be7e26911 --- /dev/null +++ b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/mapper/EsbDeadLetterMapper.java @@ -0,0 +1,9 @@ +package com.healthlink.his.esb.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.healthlink.his.esb.domain.EsbDeadLetter; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface EsbDeadLetterMapper extends BaseMapper { +} diff --git a/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/mapper/EsbMonitorStatsMapper.java b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/mapper/EsbMonitorStatsMapper.java new file mode 100644 index 000000000..7a64c3b86 --- /dev/null +++ b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/mapper/EsbMonitorStatsMapper.java @@ -0,0 +1,9 @@ +package com.healthlink.his.esb.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.healthlink.his.esb.domain.EsbMonitorStats; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface EsbMonitorStatsMapper extends BaseMapper { +} diff --git a/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/IEsbDeadLetterService.java b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/IEsbDeadLetterService.java new file mode 100644 index 000000000..afd024233 --- /dev/null +++ b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/IEsbDeadLetterService.java @@ -0,0 +1,7 @@ +package com.healthlink.his.esb.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.healthlink.his.esb.domain.EsbDeadLetter; + +public interface IEsbDeadLetterService extends IService { +} diff --git a/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/IEsbMonitorStatsService.java b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/IEsbMonitorStatsService.java new file mode 100644 index 000000000..f66a23583 --- /dev/null +++ b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/IEsbMonitorStatsService.java @@ -0,0 +1,7 @@ +package com.healthlink.his.esb.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.healthlink.his.esb.domain.EsbMonitorStats; + +public interface IEsbMonitorStatsService extends IService { +} diff --git a/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/impl/EsbDeadLetterServiceImpl.java b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/impl/EsbDeadLetterServiceImpl.java new file mode 100644 index 000000000..0da5203fd --- /dev/null +++ b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/impl/EsbDeadLetterServiceImpl.java @@ -0,0 +1,11 @@ +package com.healthlink.his.esb.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.healthlink.his.esb.domain.EsbDeadLetter; +import com.healthlink.his.esb.mapper.EsbDeadLetterMapper; +import com.healthlink.his.esb.service.IEsbDeadLetterService; +import org.springframework.stereotype.Service; + +@Service +public class EsbDeadLetterServiceImpl extends ServiceImpl implements IEsbDeadLetterService { +} diff --git a/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/impl/EsbMonitorStatsServiceImpl.java b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/impl/EsbMonitorStatsServiceImpl.java new file mode 100644 index 000000000..4739dbae5 --- /dev/null +++ b/healthlink-his-server/healthlink-his-domain/src/main/java/com/healthlink/his/esb/service/impl/EsbMonitorStatsServiceImpl.java @@ -0,0 +1,11 @@ +package com.healthlink.his.esb.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.healthlink.his.esb.domain.EsbMonitorStats; +import com.healthlink.his.esb.mapper.EsbMonitorStatsMapper; +import com.healthlink.his.esb.service.IEsbMonitorStatsService; +import org.springframework.stereotype.Service; + +@Service +public class EsbMonitorStatsServiceImpl extends ServiceImpl implements IEsbMonitorStatsService { +} diff --git a/healthlink-his-ui/src/views/esbmanage/reliability/api.js b/healthlink-his-ui/src/views/esbmanage/reliability/api.js new file mode 100644 index 000000000..f5cf9d97f --- /dev/null +++ b/healthlink-his-ui/src/views/esbmanage/reliability/api.js @@ -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})} diff --git a/healthlink-his-ui/src/views/esbmanage/reliability/index.vue b/healthlink-his-ui/src/views/esbmanage/reliability/index.vue new file mode 100644 index 000000000..dc6902927 --- /dev/null +++ b/healthlink-his-ui/src/views/esbmanage/reliability/index.vue @@ -0,0 +1,70 @@ + +