Преглед изворни кода

Merge remote-tracking branch 'origin/saas-api' into saas-api

ct пре 3 дана
родитељ
комит
64a2adcc52

+ 41 - 4
fs-company/src/main/java/com/fs/company/controller/workflow/LobsterWorkflowExecController.java

@@ -15,6 +15,8 @@ import com.fs.company.service.workflow.LobsterWorkflowExecutor;
 import com.fs.company.service.workflow.SemanticTakeoverDetector;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -26,6 +28,8 @@ import java.util.Map;
 @RequestMapping("/workflow/lobster-exec")
 public class LobsterWorkflowExecController extends BaseController {
 
+    private static final Logger logger = LoggerFactory.getLogger(LobsterWorkflowExecController.class);
+
     @Autowired
     private LobsterWorkflowExecutor workflowExecutor;
 
@@ -259,13 +263,46 @@ public class LobsterWorkflowExecController extends BaseController {
         String content = (String) params.getOrDefault("content", "");
         Long templateId = params.get("templateId") != null ? Long.valueOf(params.get("templateId").toString()) : null;
 
-        // 模拟对话响应(后续可对接真实的 LobsterWorkflowExecutor 模拟执行能力)
+        // 对接真实的龙虾工作流模拟执行
+        if (templateId != null && content != null && !content.isEmpty()) {
+            try {
+                // 发起一次模拟对话:启动临时实例 → 执行首个节点 → 返回AI回复
+                Map<String, Object> initVars = new java.util.HashMap<>();
+                initVars.put("simulate_mode", true);
+                initVars.put("customer_input", content);
+                AjaxResult startResult = workflowExecutor.startWorkflow(companyId, templateId, 0L, initVars);
+                if (startResult != null && Integer.valueOf(200).equals(startResult.get("code"))) {
+                    // startWorkflow 返回的 data 可能包含首个节点的回复
+                    Map<String, Object> reply = new java.util.HashMap<>();
+                    Object data = startResult.get("data");
+                    if (data instanceof Map) {
+                        @SuppressWarnings("unchecked")
+                        Map<String, Object> dataMap = (Map<String, Object>) data;
+                        Long instanceId = dataMap.get("instanceId") != null
+                                ? Long.valueOf(dataMap.get("instanceId").toString()) : null;
+                        reply.put("instanceId", instanceId);
+                        reply.put("reply", dataMap.getOrDefault("reply", dataMap.getOrDefault("message",
+                                "模拟对话已启动,请查看实例执行日志")));
+                    } else {
+                        reply.put("reply", "模拟对话已启动");
+                    }
+                    reply.put("templateId", templateId);
+                    reply.put("companyId", companyId);
+                    reply.put("timestamp", System.currentTimeMillis());
+                    reply.put("mode", "simulate");
+                    return AjaxResult.success(reply);
+                }
+            } catch (Exception e) {
+                logger.error("[Simulate] 模拟对话失败: companyId={}, templateId={}", companyId, templateId, e);
+            }
+        }
+
+        // 降级:无模板时返回提示
         Map<String, Object> reply = new java.util.HashMap<>();
-        reply.put("reply", "[模拟回复] \u300c" + content + "\u300d — 模板ID:" + (templateId != null ? templateId : "N/A"));
-        reply.put("templateId", templateId);
+        reply.put("reply", "请提供有效的模板ID和对话内容");
+        reply.put("mode", "simulate");
         reply.put("companyId", companyId);
         reply.put("timestamp", System.currentTimeMillis());
-        reply.put("mode", "simulate");
         return AjaxResult.success(reply);
     }
 }

+ 0 - 10
fs-service/pom.xml

@@ -310,16 +310,6 @@
             <version>${org.mapstruct.version}</version>
         </dependency>
 
-        <dependency>
-            <groupId>org.mapstruct</groupId>
-            <artifactId>mapstruct</artifactId>
-            <version>${org.mapstruct.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.mapstruct</groupId>
-            <artifactId>mapstruct-processor</artifactId>
-            <version>${org.mapstruct.version}</version>
-        </dependency>
         <dependency>
             <groupId>com.hc</groupId>
             <artifactId>openapi</artifactId>

+ 0 - 16
fs-service/src/main/java/com/fs/company/service/workflow/contact/ContactInfo.java

@@ -53,20 +53,4 @@ public class ContactInfo {
         return info;
     }
 
-    public Long getCompanyId() { return companyId; }
-    public void setCompanyId(Long companyId) { this.companyId = companyId; }
-    public Long getContactId() { return contactId; }
-    public void setContactId(Long contactId) { this.contactId = contactId; }
-    public String getChannelType() { return channelType; }
-    public void setChannelType(String channelType) { this.channelType = channelType; }
-    public String getChannelUserId() { return channelUserId; }
-    public void setChannelUserId(String channelUserId) { this.channelUserId = channelUserId; }
-    public String getName() { return name; }
-    public void setName(String name) { this.name = name; }
-    public String getAvatar() { return avatar; }
-    public void setAvatar(String avatar) { this.avatar = avatar; }
-    public String getPhone() { return phone; }
-    public void setPhone(String phone) { this.phone = phone; }
-    public Map<String, Object> getExtra() { return extra; }
-    public void setExtra(Map<String, Object> extra) { this.extra = extra; }
 }

+ 117 - 0
fs-service/src/main/java/com/fs/company/service/workflow/heartbeat/impl/HeartbeatSchedulerImpl.java

@@ -10,6 +10,7 @@ import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
 
 @Service
 public class HeartbeatSchedulerImpl implements HeartbeatScheduler {
@@ -19,6 +20,12 @@ public class HeartbeatSchedulerImpl implements HeartbeatScheduler {
     @Autowired(required = false)
     private LobsterAuxiliaryMapper auxMapper;
 
+    /** 活跃实例心跳配置缓存: instanceId → HeartbeatConfig */
+    private final Map<Long, HeartbeatConfig> activeInstances = new ConcurrentHashMap<>();
+
+    /** 实例最后心跳时间缓存: instanceId → lastHeartbeatTime */
+    private final Map<Long, Long> lastHeartbeatTimes = new ConcurrentHashMap<>();
+
     /** 每5分钟发送心跳 */
     @Scheduled(cron = "0 */5 * * * ?")
     public void sendHeartbeat() {
@@ -28,6 +35,113 @@ public class HeartbeatSchedulerImpl implements HeartbeatScheduler {
         } catch (Exception e) { log.warn("[Heartbeat] 心跳发送失败: {}", e.getMessage()); }
     }
 
+    @Override
+    public void registerInstance(Long companyId, Long instanceId, HeartbeatConfig config) {
+        if (instanceId == null) return;
+        activeInstances.put(instanceId, config);
+        lastHeartbeatTimes.put(instanceId, System.currentTimeMillis());
+        if (auxMapper != null) {
+            try {
+                auxMapper.insertHeartbeat(companyId, "instance_" + instanceId, "registered");
+            } catch (Exception e) { log.debug("[Heartbeat] 注册实例心跳记录失败: {}", e.getMessage()); }
+        }
+        log.info("[Heartbeat] 工作流实例已注册: instanceId={}, timeoutMs={}", instanceId,
+                config != null ? config.getTimeoutMs() : 86400000L);
+    }
+
+    @Override
+    public void unregisterInstance(Long instanceId) {
+        activeInstances.remove(instanceId);
+        lastHeartbeatTimes.remove(instanceId);
+        log.info("[Heartbeat] 工作流实例已注销: instanceId={}", instanceId);
+    }
+
+    @Override
+    public void checkAndExecute() {
+        long now = System.currentTimeMillis();
+        List<Long> timeoutInstances = new ArrayList<>();
+
+        for (Map.Entry<Long, Long> entry : lastHeartbeatTimes.entrySet()) {
+            Long instanceId = entry.getKey();
+            Long lastHb = entry.getValue();
+            HeartbeatConfig config = activeInstances.get(instanceId);
+            long timeoutMs = (config != null ? config.getTimeoutMs() : 86400000L);
+
+            if (now - lastHb > timeoutMs) {
+
+                timeoutInstances.add(instanceId);
+                log.warn("[Heartbeat] 实例心跳超时: instanceId={}, lastHb={}, timeoutMs={}",
+                        instanceId, new Date(lastHb), timeoutMs);
+            }
+        }
+
+        // 清理超时实例
+        for (Long instanceId : timeoutInstances) {
+            unregisterInstance(instanceId);
+        }
+
+        if (!timeoutInstances.isEmpty()) {
+            log.info("[Heartbeat] 清理超时实例: {} 个", timeoutInstances.size());
+        }
+    }
+
+    @Override
+    public Map<String, Object> getHeartbeatStatus(Long instanceId) {
+        Map<String, Object> status = new LinkedHashMap<>();
+        status.put("instanceId", instanceId);
+
+        if (instanceId == null) {
+            status.put("status", "unknown");
+            status.put("active", false);
+            status.put("message", "实例ID为空");
+            return status;
+        }
+
+        boolean isRegistered = activeInstances.containsKey(instanceId);
+        Long lastHb = lastHeartbeatTimes.get(instanceId);
+
+        status.put("registered", isRegistered);
+
+        if (isRegistered && lastHb != null) {
+            long now = System.currentTimeMillis();
+            HeartbeatConfig config = activeInstances.get(instanceId);
+            long timeoutMs = (config != null ? config.getTimeoutMs() : 86400000L);
+            long elapsed = now - lastHb;
+
+            status.put("lastHeartbeatTime", new Date(lastHb).toString());
+            status.put("lastHeartbeatTimestamp", lastHb);
+            status.put("elapsedMinutes", elapsed / 60000);
+            status.put("timeoutMinutes", timeoutMs / 60000);
+            status.put("active", elapsed < timeoutMs);
+            status.put("status", elapsed < timeoutMs ? "active" : "timeout");
+
+            if (elapsed >= timeoutMs) {
+                status.put("message", "实例心跳已超时 " + (elapsed / 60000) + " 分钟");
+            } else {
+                status.put("message", "实例心跳正常");
+            }
+        } else {
+            status.put("active", false);
+            status.put("status", "unregistered");
+            status.put("message", "实例未注册或已注销");
+
+            // 尝试从数据库查询历史心跳记录
+            if (auxMapper != null) {
+                try {
+                    List<Map<String, Object>> records = auxMapper.selectHeartbeats(0L,
+                            "instance_" + instanceId, 5);
+                    if (records != null && !records.isEmpty()) {
+                        status.put("status", "history_only");
+                        status.put("message", "实例未在内存中注册,但存在历史心跳记录");
+                        status.put("historyRecords", records.size());
+                    }
+                } catch (Exception e) { log.debug("[Heartbeat] 查询历史心跳失败: {}", e.getMessage()); }
+            }
+        }
+
+        return status;
+    }
+
     /** 每15分钟清理过期心跳(1小时前) */
     @Scheduled(cron = "0 */15 * * * ?")
     public void cleanupExpiredHeartbeats() {
@@ -35,6 +149,9 @@ public class HeartbeatSchedulerImpl implements HeartbeatScheduler {
         try {
             log.debug("[Heartbeat] 清理过期心跳记录");
         } catch (Exception e) { log.warn("[Heartbeat] 清理失败: {}", e.getMessage()); }
+
+        // 同时执行超时检查
+        checkAndExecute();
     }
 
     public void recordHeartbeat(Long companyId, String taskKey, String status) {

+ 171 - 11
fs-service/src/main/java/com/fs/company/service/workflow/identity/impl/IdentityHidingServiceImpl.java

@@ -7,8 +7,16 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-import java.util.Map;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
 
+/**
+ * 机器人身份隐藏服务实现
+ *
+ * 核心能力:确保龙虾引擎在沟通过程中不暴露机器人身份,
+ * 让客户感觉是在和真人沟通
+ */
 @Service
 public class IdentityHidingServiceImpl implements IdentityHidingService {
 
@@ -17,43 +25,195 @@ public class IdentityHidingServiceImpl implements IdentityHidingService {
     @Autowired(required = false)
     private LobsterAuxiliaryMapper auxMapper;
 
-    public String getSafeHandoffMessage(String defaultMsg) { return defaultMsg != null ? defaultMsg : "正在为您转接人工客服..."; }
-    public String getSafeSystemPrompt() { return "你是专业的销售顾问"; }
-    public String getSafeErrorMessage(Object ctx, String code) { return "系统繁忙,请稍后再试"; }
-    public String hideIdentity(String text) { return text; }
+    /** 暴露机器人身份的关键词模式 */
+    private static final List<Pattern> IDENTITY_LEAK_PATTERNS = Arrays.asList(
+        Pattern.compile("作为.{0,3}(AI|人工智能|机器人|语言模型|大模型)"),
+        Pattern.compile("我是.{0,3}(AI|人工智能|机器人|语言模型|助手|大模型)"),
+        Pattern.compile("(系统|程序|算法|模型).{0,3}(自动|生成|回复)"),
+        Pattern.compile("抱歉.{0,5}(无法理解|不能回答|无法处理)"),
+        Pattern.compile("请稍等.{0,5}(处理|查询|核实|确认)"),
+        Pattern.compile("(AI|人工智能|机器人|系统)"),
+        Pattern.compile("(自动|程序化|机械)")
+    );
+
+    /** 替换话术模板 */
+    private static final String[] HUMAN_FILLERS = {
+        "我看看", "让我想想", "这个嘛", "嗯,", "其实呢,",
+        "我帮您看一下", "稍等一下哈", "好的呢", "没问题~"
+    };
+
+    /** 租户级配置缓存 */
+    private final Map<Long, Map<String, Object>> configCache = new ConcurrentHashMap<>();
 
     @Override
     public String hideIdentity(Long companyId, String message, Map<String, Object> context) {
-        return "";
+        if (message == null || message.isEmpty()) return message;
+
+        String result = message;
+
+        // 1. 替换暴露身份的关键词
+        for (Pattern pattern : IDENTITY_LEAK_PATTERNS) {
+            result = pattern.matcher(result).replaceAll("我");
+        }
+
+        // 2. 替换AI常见套话
+        result = result.replace("根据我的理解", "依我看")
+                       .replace("根据数据分析", "了解后发现")
+                       .replace("基于以上信息", "总的来说")
+                       .replace("为您生成以下回复", "")
+                       .replace("以下是回复内容", "");
+
+        // 3. 清理残留AI标签
+        result = result.replace("[AI]", "").replace("[自动]", "").replace("[系统]", "");
+
+        // 4. 确保回复不为空
+        if (result.trim().isEmpty()) {
+            result = getSafeErrorMessage(companyId, "empty_reply");
+        }
+
+        return result.trim();
     }
 
     @Override
     public String getSafeSystemPrompt(Long companyId) {
-        return "";
+        // 优先从租户配置获取
+        Map<String, Object> config = getIdentityConfig(companyId);
+        if (config != null && config.containsKey("systemRole")) {
+            return (String) config.get("systemRole");
+        }
+
+        // 从数据库获取
+        if (auxMapper != null) {
+            try {
+                List<Map<String, Object>> prompts = auxMapper.selectDynamicImpls(companyId, 99);
+                if (prompts != null && !prompts.isEmpty()) {
+                    String role = (String) prompts.get(0).get("script_content");
+                    if (role != null && !role.isEmpty()) return role;
+                }
+            } catch (Exception e) {
+                logger.debug("[IdentityHide] 加载系统角色失败: {}", e.getMessage());
+            }
+        }
+
+        // 默认兜底角色
+        return "你是一位经验丰富的专业顾问,请用自然口语化的方式与客户沟通。"
+             + "不要使用'作为AI'、'根据算法'、'系统分析'等暴露非真人身份的措辞。"
+             + "回复要像真人一样有停顿、有思考、有情感。"
+             + "碰到不确定的问题时,说'我帮您确认一下'而不是'我无法回答'。";
     }
 
     @Override
     public String getSafeHandoffMessage(Long companyId, String urgency) {
-        return "";
+        if (urgency == null) urgency = "medium";
+
+        // 根据紧急程度选择不同话术
+        switch (urgency.toLowerCase()) {
+            case "high":
+            case "urgent":
+                return "您稍等,我马上帮您联系更专业的人来处理这个问题。";
+            case "low":
+                return "这个我帮您记下了,后续有专人跟进。";
+            case "medium":
+            default:
+                return "您这个问题很专业,我帮您安排专属顾问来对接,稍等一下~";
+        }
     }
 
     @Override
     public String getSafeErrorMessage(Long companyId, String errorType) {
-        return "";
+        if (errorType == null) errorType = "unknown";
+
+        switch (errorType.toLowerCase()) {
+            case "timeout":
+                return "刚才有点卡,麻烦您再说一遍?";
+            case "overload":
+                return "这会儿咨询的人有点多,我缓一下马上回复您~";
+            case "empty_reply":
+                return "嗯,我再确认一下具体情况。";
+            case "unknown":
+            default:
+                return "不好意思,我需要确认一下,稍等哈~";
+        }
     }
 
     @Override
     public long calculateHumanLikeDelay(String message, Map<String, Object> context) {
-        return 0;
+        if (message == null || message.isEmpty()) return 1000L;
+
+        int length = message.length();
+        // 模拟真人打字速度:中文约3字/秒,加上思考时间
+        long baseDelay = length * 300L; // 每字符300ms
+
+        // 消息越长,思考时间越多
+        if (length > 100) baseDelay += 2000L;  // 长回复,多思考
+        else if (length > 50) baseDelay += 1000L;
+        else baseDelay += 500L;
+
+        // 随机波动 ±30%,避免过于机械
+        double jitter = 0.7 + Math.random() * 0.6;
+        long result = (long) (baseDelay * jitter);
+
+        // 限制在1-10秒之间
+        return Math.max(1000L, Math.min(10000L, result));
     }
 
     @Override
     public Map<String, Object> getIdentityConfig(Long companyId) {
-        return Map.of();
+        // 先从缓存获取
+        Map<String, Object> cached = configCache.get(companyId);
+        if (cached != null) return cached;
+
+        Map<String, Object> config = new LinkedHashMap<>();
+        // 默认配置
+        config.put("hideIdentity", true);
+        config.put("systemRole", null);
+        config.put("humanLikeDelay", true);
+        config.put("delayMinMs", 1000);
+        config.put("delayMaxMs", 8000);
+        config.put("useFillers", true);
+        config.put("strictMode", false);
+
+        // 从数据库加载
+        if (auxMapper != null) {
+            try {
+                List<Map<String, Object>> rows = auxMapper.selectDynamicImpls(companyId, 98);
+                if (rows != null && !rows.isEmpty()) {
+                    String content = (String) rows.get(0).get("script_content");
+                    if (content != null && !content.isEmpty()) {
+                        try {
+                            @SuppressWarnings("unchecked")
+                            Map<String, Object> dbConfig =
+                                com.alibaba.fastjson.JSON.parseObject(content, Map.class);
+                            if (dbConfig != null) config.putAll(dbConfig);
+                        } catch (Exception ignored) {}
+                    }
+                }
+            } catch (Exception e) {
+                logger.debug("[IdentityHide] 加载配置失败: {}", e.getMessage());
+            }
+        }
+
+        configCache.put(companyId, config);
+        return config;
     }
 
     @Override
     public void updateIdentityConfig(Long companyId, Map<String, Object> config) {
+        if (config == null) return;
+        // 更新缓存
+        Map<String, Object> existing = getIdentityConfig(companyId);
+        existing.putAll(config);
+        configCache.put(companyId, existing);
 
+        // 持久化到数据库
+        if (auxMapper != null) {
+            try {
+                String json = com.alibaba.fastjson.JSON.toJSONString(existing);
+                auxMapper.insertDynamicImpl(companyId, 98, "identity_config", "identity_config",
+                        json, "ACTIVE");
+            } catch (Exception e) {
+                logger.error("[IdentityHide] 保存配置失败: {}", e.getMessage());
+            }
+        }
     }
 }

+ 57 - 1
fs-service/src/main/java/com/fs/company/service/workflow/learning/impl/TenantLearningEngineImpl.java

@@ -206,7 +206,63 @@ public class TenantLearningEngineImpl implements TenantLearningEngine {
         } catch (Exception e) { return 0; }
     }
 
-    private int analyzeCustomerProfileCorrelations(Long companyId) { return 0; }
+    private int analyzeCustomerProfileCorrelations(Long companyId) {
+        if (learningMapper == null) return 0;
+        try {
+            // 从replay buffer中分析客户画像与对话效果的关联
+            List<Map<String, Object>> events = learningMapper.selectReplayBuffer(companyId);
+            if (events == null || events.isEmpty()) return 0;
+
+            int discoveries = 0;
+            Map<String, Integer> profileStats = new LinkedHashMap<>();
+            Map<String, Integer> profileWins = new LinkedHashMap<>();
+
+            for (Map<String, Object> event : events) {
+                String customerMessage = (String) event.get("customer_message");
+                Integer qualityScore = event.get("quality_score") instanceof Number
+                        ? ((Number) event.get("quality_score")).intValue() : null;
+
+                if (customerMessage == null || customerMessage.isEmpty()) continue;
+
+                String profileTag = inferProfileTag(customerMessage);
+                profileStats.merge(profileTag, 1, Integer::sum);
+                if (qualityScore != null && qualityScore >= 120) {
+                    profileWins.merge(profileTag, 1, Integer::sum);
+                }
+            }
+
+            for (String tag : profileStats.keySet()) {
+                int total = profileStats.getOrDefault(tag, 0);
+                int wins = profileWins.getOrDefault(tag, 0);
+                if (total >= 3) {
+                    double rate = wins * 100.0 / total;
+                    String insight = "画像[" + tag + "]: 总交互" + total + "次,高质量率" + String.format("%.0f%%", rate);
+                    learningMapper.upsertPattern(companyId, "profile", tag, insight, rate / 100.0, "ProfileAnalyzer");
+                    discoveries++;
+                }
+            }
+
+            return discoveries;
+        } catch (Exception e) { return 0; }
+    }
+
+    /** 从消息内容中推断客户画像标签 */
+    private String inferProfileTag(String message) {
+        if (message == null || message.isEmpty()) return "unknown";
+        if (message.contains("价格") || message.contains("优惠") || message.contains("便宜") || message.contains("贵"))
+            return "price_sensitive";
+        if (message.contains("品牌") || message.contains("质量") || message.contains("正品"))
+            return "quality_seeker";
+        if (message.contains("急") || message.contains("快") || message.contains("马上") || message.contains("立刻"))
+            return "urgent";
+        if (message.contains("推荐") || message.contains("哪个好") || message.contains("帮我选"))
+            return "guidance_seeker";
+        if (message.contains("退货") || message.contains("退款") || message.contains("投诉"))
+            return "at_risk";
+        if (message.contains("可以") || message.contains("好") || message.contains("喜欢") || message.contains("不错"))
+            return "satisfied";
+        return "general";
+    }
 
     private int generateStrategyRecommendations(Long companyId) {
         if (learningMapper == null) return 0;

+ 36 - 2
fs-service/src/main/java/com/fs/company/service/workflow/queue/DeadLetterQueue.java

@@ -61,12 +61,46 @@ public class DeadLetterQueue {
         return result;
     }
 
+    /**
+     * 获取待重试死信数量(retry_count < max_retries 的消息)
+     */
     public int getPendingCount() {
-        return getDeadLetterList().size();
+        if (auxMapper == null) return 0;
+        int pending = 0;
+        try {
+            List<Map<String, Object>> rows = auxMapper.selectDeadLetters(null, 500);
+            if (rows != null) {
+                for (Map<String, Object> row : rows) {
+                    Object retryCount = row.get("retry_count");
+                    int count = retryCount instanceof Number ? ((Number) retryCount).intValue() : 0;
+                    if (count < 3) pending++;
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("[DeadLetter] getPendingCount failed: {}", e.getMessage());
+        }
+        return pending;
     }
 
+    /**
+     * 获取已死消息数量(retry_count >= max_retries 的消息,需人工处理)
+     */
     public int getDeadCount() {
-        return getDeadLetterList().size();
+        if (auxMapper == null) return 0;
+        int dead = 0;
+        try {
+            List<Map<String, Object>> rows = auxMapper.selectDeadLetters(null, 500);
+            if (rows != null) {
+                for (Map<String, Object> row : rows) {
+                    Object retryCount = row.get("retry_count");
+                    int count = retryCount instanceof Number ? ((Number) retryCount).intValue() : 0;
+                    if (count >= 3) dead++;
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("[DeadLetter] getDeadCount failed: {}", e.getMessage());
+        }
+        return dead;
     }
 
     public int retryAllDead(Predicate<DeadMessage> filter) {

+ 1 - 1
pom.xml

@@ -31,7 +31,7 @@
         <velocity.version>1.7</velocity.version>
         <jwt.version>0.9.1</jwt.version>
         <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
-        <lombok.version>1.18.32</lombok.version>
+        <lombok.version>1.18.30</lombok.version>
         <gson-version>2.10</gson-version>
         <ijpay-version>2.7.8</ijpay-version>
     </properties>