浏览代码

文本审核

mazhenhang 1 月之前
父节点
当前提交
7509d94b14

+ 68 - 1
LiangZhiYUMao/pages/message/chat.vue

@@ -1112,7 +1112,51 @@ export default {
       const content = this.inputText;
       this.inputText = '';
       
-      // 先检查是否被拉黑
+      // 1. 先进行消息内容审核
+      try {
+        const checkRes = await uni.request({
+          url: 'http://localhost:1004/api/chat/checkMessage',
+          method: 'POST',
+          data: {
+            userId: String(this.userId),
+            content: content
+          },
+          header: {
+            'Content-Type': 'application/json'
+          }
+        });
+        
+        if (checkRes[1].data.code !== 200) {
+          // 审核未通过,显示失败消息
+          const failedMessage = {
+            messageId: 'failed_' + Date.now(),
+            fromUserId: this.userId,
+            toUserId: this.targetUserId,
+            messageType: 1,
+            content: content,
+            sendStatus: 4, // 4表示发送失败
+            failReason: 'audit_failed', // 标记失败原因:审核失败
+            sendTime: new Date(),
+            fromUserName: '我'
+          };
+          
+          this.messages.push(failedMessage);
+          this.scrollToBottom();
+          
+          uni.showToast({
+            title: checkRes[1].data.message || '消息发送失败',
+            icon: 'none',
+            duration: 2000
+          });
+          
+          return;
+        }
+      } catch (error) {
+        console.error('❌ 消息审核失败:', error);
+        // 审核接口失败,为了不影响用户体验,继续发送
+      }
+      
+      // 2. 检查是否被拉黑
       const isBlocked = await this.checkIsBlockedByTarget();
       
       if (isBlocked) {
@@ -1605,6 +1649,19 @@ export default {
     handleMessageClick(msg) {
       // 如果是发送失败的消息,提示重试
       if (msg.sendStatus === 4) {
+        // 检查失败原因
+        if (msg.failReason === 'audit_failed') {
+          // 审核失败的消息不允许重试
+          uni.showModal({
+            title: '无法重试',
+            content: '该消息包含敏感信息,无法发送',
+            showCancel: false,
+            confirmText: '知道了'
+          });
+          return;
+        }
+        
+        // 其他原因失败的消息可以重试
         uni.showModal({
           title: '发送失败',
           content: '消息发送失败,是否重试?',
@@ -1626,6 +1683,16 @@ export default {
         return;
       }
       
+      // 双重保护:检查是否是审核失败的消息
+      if (msg.failReason === 'audit_failed') {
+        uni.showToast({
+          title: '该消息包含敏感信息,无法重试',
+          icon: 'none',
+          duration: 2000
+        });
+        return;
+      }
+      
       const isBlocked = await this.checkIsBlockedByTarget();
       if (isBlocked) {
         const index = this.messages.findIndex(m => m.messageId === msg.messageId);

+ 88 - 16
LiangZhiYUMao/utils/tim-presence-manager.js

@@ -1,12 +1,10 @@
 /**
- * 基于 WebSocket + 腾讯 IM 的用户在线状态管理器
- * 核心思路:
- * 1. 利用腾讯 IM SDK 感知自身和好友的 IM 连接状态
- * 2. 通过 WebSocket 向服务端上报 IM 状态变更
- * 3. 通过 WebSocket 接收服务端推送的其他用户状态变更
+ * TIM在线状态管理器 + WebSocket聊天管理器
+ * 统一管理WebSocket连接、心跳、在线状态、聊天消息
  */
 
 import timManager from './tim-manager.js';
+import TIM from 'tim-wx-sdk';
 
 class TIMPresenceManager {
   constructor() {
@@ -19,13 +17,21 @@ class TIMPresenceManager {
     this.userId = null;
     this.statusCallbacks = new Map(); // 存储状态变化回调
     this.onlineStatusCache = new Map(); // 缓存在线状态
-    
-    // WebSocket服务器地址(通过网关)
     this.wsUrl = 'ws://localhost:8083/ws/chat';
     
     // TIM 状态监听器引用(用于清理)
     this.timStatusListener = null;
     this.timConnectListener = null;
+    
+    // 聊天消息相关
+    this.messageQueue = []; // 消息队列(断线时暂存)
+    this.maxQueueSize = 100;
+    this.onMessageCallback = null; // 消息回调
+    this.onConnectCallback = null; // 连接回调
+    this.onDisconnectCallback = null; // 断开回调
+    this.onErrorCallback = null; // 错误回调
+    this.reconnectCount = 0;
+    this.maxReconnectCount = 10;
   }
   
   /**
@@ -34,12 +40,16 @@ class TIMPresenceManager {
   async init(userId) {
     this.userId = userId;
     
-    console.log('🚀 初始化 tim-presence-manager,用户ID:', userId);
+    console.log('🚀 ========== 初始化 tim-presence-manager ==========');
+    console.log('🚀 用户ID:', userId);
+    console.log('🚀 WebSocket URL:', this.wsUrl);
     
     // 连接 WebSocket(接收服务端推送的状态变更)
+    console.log('🚀 开始连接 WebSocket...');
     this.connectWebSocket();
     
-    console.log('✅ tim-presence-manager 初始化完成');
+    console.log('✅ tim-presence-manager 初始化完成(WebSocket连接中...)');
+    console.log('================================================');
   }
   
   /**
@@ -52,6 +62,15 @@ class TIMPresenceManager {
       console.log('   当前用户ID:', this.userId);
       console.log('   WebSocket URL:', wsUrl);
       
+      // 先关闭旧连接
+      if (this.ws) {
+        try {
+          uni.closeSocket();
+        } catch (e) {
+          console.warn('关闭旧连接失败:', e);
+        }
+      }
+      
       this.ws = uni.connectSocket({
         url: wsUrl,
         success: () => {
@@ -64,8 +83,8 @@ class TIMPresenceManager {
         }
       });
       
-      // 监听连接打开
-      uni.onSocketOpen((res) => {
+      // 使用 SocketTask 对象的监听器(推荐方式)
+      this.ws.onOpen((res) => {
         console.log('🎉 ========== WebSocket 连接成功 ==========');
         console.log('   响应数据:', res);
         console.log('   用户ID:', this.userId);
@@ -73,19 +92,31 @@ class TIMPresenceManager {
         console.log('==========================================');
         
         this.isConnected = true;
+        this.reconnectCount = 0; // 重置重连计数
+        
+        // 启动心跳
         this.startHeartbeat();
         
+        // 发送队列中的消息
+        this.flushMessageQueue();
+        
         // 连接成功后,立即上报当前 TIM 连接状态
         this.reportCurrentTIMStatus();
+        
+        // 触发连接回调
+        if (this.onConnectCallback) {
+          this.onConnectCallback();
+        }
       });
       
       // 监听消息
-      uni.onSocketMessage((res) => {
+      this.ws.onMessage((res) => {
+        console.log('📨 收到WebSocket消息:', res.data);
         this.handleMessage(res.data);
       });
       
       // 监听错误
-      uni.onSocketError((err) => {
+      this.ws.onError((err) => {
         console.error('❌ ========== WebSocket 错误 ==========');
         console.error('   错误信息:', err);
         console.error('   错误详情:', JSON.stringify(err));
@@ -94,11 +125,17 @@ class TIMPresenceManager {
         console.error('======================================');
         
         this.isConnected = false;
+        
+        // 触发错误回调
+        if (this.onErrorCallback) {
+          this.onErrorCallback(err);
+        }
+        
         this.scheduleReconnect();
       });
       
       // 监听关闭
-      uni.onSocketClose((res) => {
+      this.ws.onClose((res) => {
         console.log('🔌 ========== WebSocket 连接关闭 ==========');
         console.log('   关闭信息:', res);
         console.log('   关闭码:', res?.code);
@@ -108,6 +145,12 @@ class TIMPresenceManager {
         
         this.isConnected = false;
         this.stopHeartbeat();
+        
+        // 触发断开回调
+        if (this.onDisconnectCallback) {
+          this.onDisconnectCallback();
+        }
+        
         this.scheduleReconnect();
       });
       
@@ -406,7 +449,7 @@ class TIMPresenceManager {
       const message = typeof data === 'string' ? JSON.parse(data) : data;
       
       switch (message.type) {
-        case 'PONG':
+        case 'pong':  // 改为小写,匹配后端
           // 心跳响应
           console.log('💓 收到心跳响应');
           break;
@@ -445,11 +488,24 @@ class TIMPresenceManager {
     
     this.heartbeatTimer = setInterval(() => {
       if (this.isConnected) {
+        console.log('💓 ========== 发送心跳PING ==========');
+        console.log('💓 用户ID:', this.userId);
+        console.log('💓 WebSocket连接状态:', this.isConnected);
+        console.log('💓 发送消息:', {
+          type: 'ping',
+          fromUserId: this.userId,
+          timestamp: Date.now()
+        });
+        
         this.sendMessage({
-          type: 'PING',
+          type: 'ping',  // 改为小写,匹配后端
           fromUserId: this.userId,
           timestamp: Date.now()
         });
+        
+        console.log('💓 ====================================');
+      } else {
+        console.warn('⚠️ WebSocket未连接,跳过心跳');
       }
     }, this.heartbeatInterval);
   }
@@ -563,6 +619,22 @@ class TIMPresenceManager {
     }, this.reconnectInterval);
   }
   
+  /**
+   * 发送队列中的消息
+   */
+  flushMessageQueue() {
+    if (this.messageQueue.length === 0) {
+      return;
+    }
+    
+    console.log(`📤 发送队列中的 ${this.messageQueue.length} 条消息`);
+    
+    while (this.messageQueue.length > 0) {
+      const message = this.messageQueue.shift();
+      this.sendMessage(message);
+    }
+  }
+  
   /**
    * 断开连接
    */

+ 46 - 0
service/websocket/src/main/java/com/zhentao/controller/ChatController.java

@@ -6,6 +6,7 @@ import com.zhentao.entity.ChatMessage;
 import com.zhentao.service.ChatMessageService;
 import com.zhentao.service.OnlineUserService;
 import com.zhentao.service.UserVipService;
+import com.zhentao.utils.ContactFilter;
 import com.zhentao.vo.ResultVo;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -373,4 +374,49 @@ public class ChatController {
     public ResultVo updateMessageCount(@RequestParam("userId")Long userId,@RequestParam("targetUserId")Long targetUserId){
         return userVipService.updateMessageCount(userId,targetUserId);
     }
+
+    /**
+     * 消息内容审核接口
+     * POST /api/chat/checkMessage
+     * Body: {"userId": 10001, "content": "消息内容"}
+     */
+    @PostMapping("/checkMessage")
+    public Map<String, Object> checkMessage(@RequestBody Map<String, Object> params) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            String userId = params.get("userId").toString();
+            String content = params.get("content").toString();
+            
+            if (content == null || content.trim().isEmpty()) {
+                result.put("code", 400);
+                result.put("message", "消息内容不能为空");
+                result.put("data", false);
+                return result;
+            }
+            
+            // 使用ContactFilter进行审核
+            boolean hasContact = ContactFilter.hasContact(userId, content);
+            
+            if (hasContact) {
+                System.out.println("⚠️ 消息被拦截 - 用户ID: " + userId + ", 内容: " + content);
+                result.put("code", 400);
+                result.put("message", "消息包含敏感信息,发送失败");
+                result.put("data", false);
+            } else {
+                result.put("code", 200);
+                result.put("message", "审核通过");
+                result.put("data", true);
+            }
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ 消息审核失败: " + e.getMessage());
+            result.put("code", 500);
+            result.put("message", "审核失败: " + e.getMessage());
+            result.put("data", false);
+        }
+        
+        return result;
+    }
 }

+ 16 - 2
service/websocket/src/main/java/com/zhentao/handler/ChatWebSocketHandler.java

@@ -75,15 +75,20 @@ public class ChatWebSocketHandler implements WebSocketHandler {
         }
         
         String payload = message.getPayload().toString();
-        System.out.println("收到用户 " + userId + " 的消息: " + payload);
+        System.out.println("📨 收到用户 " + userId + " 的消息: " + payload);
         
         try {
             WsMessage wsMessage = JSONUtil.toBean(payload, WsMessage.class);
             wsMessage.setFromUserId(userId);
             
+            System.out.println("📋 解析后的消息类型: [" + wsMessage.getType() + "]");
+            System.out.println("📋 期望的PING类型: [" + ChatConstants.WsMessageType.PING + "]");
+            System.out.println("📋 类型是否匹配: " + ChatConstants.WsMessageType.PING.equals(wsMessage.getType()));
+            
             // 根据消息类型处理
             switch (wsMessage.getType()) {
                 case ChatConstants.WsMessageType.PING:
+                    System.out.println("✅ 匹配到PING,调用handlePing");
                     handlePing(session, wsMessage);
                     break;
                 case ChatConstants.WsMessageType.CHAT:
@@ -102,7 +107,8 @@ public class ChatWebSocketHandler implements WebSocketHandler {
                     handleReadReceipt(session, wsMessage);
                     break;
                 default:
-                    System.err.println("未知的消息类型: " + wsMessage.getType());
+                    System.err.println("❌ 未知的消息类型: [" + wsMessage.getType() + "]");
+                    System.err.println("❌ 消息内容: " + payload);
             }
         } catch (Exception e) {
             System.err.println("处理消息异常: " + e.getMessage());
@@ -122,14 +128,22 @@ public class ChatWebSocketHandler implements WebSocketHandler {
     private void handlePing(WebSocketSession session, WsMessage message) throws IOException {
         Long userId = message.getFromUserId();
         
+        System.out.println("💓 ========== 处理心跳PING ==========");
+        System.out.println("💓 用户ID: " + userId);
+        System.out.println("💓 开始更新Redis在线状态...");
+        
         // 更新用户在线状态
         onlineUserService.updateHeartbeat(userId);
         
+        System.out.println("💓 Redis更新完成,发送PONG响应");
+        
         // 回复pong
         WsMessage pong = new WsMessage();
         pong.setType(ChatConstants.WsMessageType.PONG);
         pong.setTimestamp(System.currentTimeMillis());
         sendMessage(session, pong);
+        
+        System.out.println("💓 ====================================");
     }
 
     /**

+ 31 - 12
service/websocket/src/main/java/com/zhentao/service/OnlineUserService.java

@@ -31,15 +31,15 @@ public class OnlineUserService {
             data.put("lastHeartbeat", System.currentTimeMillis());
             data.put("onlineTime", System.currentTimeMillis());
             
-            // 设置过期时间为5分钟,需要心跳维持
-            redisTemplate.opsForValue().set(key, data, 5, TimeUnit.MINUTES);
+            // 设置过期时间为80秒,需要心跳维持
+            redisTemplate.opsForValue().set(key, data, 80, TimeUnit.SECONDS);
             
             // 将sessionId添加到用户会话集合
             String sessionKey = ChatConstants.RedisKey.USER_SESSIONS + userId;
             redisTemplate.opsForSet().add(sessionKey, sessionId);
-            redisTemplate.expire(sessionKey, 5, TimeUnit.MINUTES);
+            redisTemplate.expire(sessionKey, 80, TimeUnit.SECONDS);
             
-            System.out.println("用户 " + userId + " 已上线,sessionId: " + sessionId);
+            System.out.println("用户 " + userId + " 已上线,sessionId: " + sessionId + ",过期时间: 80秒");
         } catch (Exception e) {
             // Redis 操作失败不影响 WebSocket 连接
             System.err.println("⚠️ Redis 操作失败,但 WebSocket 连接正常: " + e.getMessage());
@@ -80,18 +80,37 @@ public class OnlineUserService {
         try {
             String key = ChatConstants.RedisKey.ONLINE_USER + userId;
             
-            Object obj = redisTemplate.opsForValue().get(key);
-            if (obj instanceof Map) {
-                @SuppressWarnings("unchecked")
-                Map<String, Object> data = (Map<String, Object>) obj;
+            System.out.println("🔄 ========== 更新心跳 ==========");
+            System.out.println("🔄 用户ID: " + userId);
+            System.out.println("🔄 Redis Key: " + key);
+            
+            // 直接刷新过期时间为80秒,返回值表示key是否存在
+            System.out.println("🔄 执行 EXPIRE 命令,设置过期时间为80秒...");
+            Boolean success = redisTemplate.expire(key, 80, TimeUnit.SECONDS);
+            
+            System.out.println("🔄 EXPIRE 命令返回结果: " + success);
+            
+            if (Boolean.FALSE.equals(success)) {
+                // key不存在(已过期),重新创建
+                System.out.println("⚠️ key不存在,重新创建...");
+                Map<String, Object> data = new HashMap<>();
                 data.put("lastHeartbeat", System.currentTimeMillis());
+                data.put("onlineTime", System.currentTimeMillis());
+                redisTemplate.opsForValue().set(key, data, 80, TimeUnit.SECONDS);
                 
-                // 刷新过期时间
-                redisTemplate.opsForValue().set(key, data, 5, TimeUnit.MINUTES);
+                System.out.println("✅ 用户 " + userId + " 在线状态已重新创建(80秒过期)");
+            } else {
+                System.out.println("✅ 用户 " + userId + " 过期时间已更新为80秒");
             }
+            
+            System.out.println("🔄 ================================");
         } catch (Exception e) {
-            // 心跳更新失败不影响连接
-            // System.err.println("心跳更新失败: " + e.getMessage());
+            // 心跳更新失败,打印日志
+            System.err.println("❌ ========== 心跳更新失败 ==========");
+            System.err.println("❌ 用户ID: " + userId);
+            System.err.println("❌ 错误信息: " + e.getMessage());
+            e.printStackTrace();
+            System.err.println("❌ ===================================");
         }
     }
 

+ 218 - 0
service/websocket/src/main/java/com/zhentao/utils/ContactFilter.java

@@ -0,0 +1,218 @@
+package com.zhentao.utils;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+
+public class ContactFilter {
+    // 消息类型枚举:新增「数字+文字混合」类型
+    private enum MsgType {
+        PURE_LETTER,        // 纯字母
+        LETTER_DIGIT,       // 字母+数字混合
+        PURE_DIGIT,         // 纯数字
+        DIGIT_TEXT_MIX,     // 数字+文字(含中文)混合(新增)
+        OTHER               // 其他类型(纯符号、纯中文等)
+    }
+
+    // 会话消息实体:存储消息内容+类型
+    private static class MsgInfo {
+        String content;
+        MsgType type;
+        int digitLength; // 新增:记录消息中的数字长度(用于动态拦截)
+
+        MsgInfo(String content, MsgType type, int digitLength) {
+            this.content = content;
+            this.type = type;
+            this.digitLength = digitLength;
+        }
+    }
+
+    // 1. 会话上下文缓存
+    private static final Map<String, Deque<MsgInfo>> USER_SESSION_CACHE = new ConcurrentHashMap<>();
+    private static final int MAX_SESSION_MSG_COUNT = 15;
+    private static final int MAX_SINGLE_LEN = 4;         // 单次数字最大长度(超4直接拦截)
+    private static final int CONSECUTIVE_TIMES_LOW = 3;  // 数字长度≤3时,连续3条拦截
+    private static final int CONSECUTIVE_TIMES_HIGH = 2; // 数字长度=4/其他类型时,连续2条拦截
+
+
+    // -------------------------- 正则/关键词配置 --------------------------
+    private static final Pattern PHONE_PATTERN = Pattern.compile(
+            "(?:(?:\\+|00)86)?1[3-9]\\d{9}|\\d{10,11}"
+    );
+
+    private static final Pattern WECHAT_PATTERN = Pattern.compile(
+            "(?:微信|微|V|vx|wx|绿泡泡|联系方式)[::\\s]*[a-zA-Z0-9][a-zA-Z0-9_]{5,19}"
+    );
+
+    private static final Set<String> SENSITIVE_KEYWORDS = new HashSet<String>() {{
+        add("加我"); add("联系方式"); add("私聊"); add("企鹅号"); add("QQ");
+        add("抖音号"); add("快手号"); add("链接"); add("二维码"); add("私信");
+        add("wx"); add("vx");add("dy");
+    }};
+
+
+    // -------------------------- 核心审核方法 --------------------------
+    public static boolean hasContact(String userId, String text) {
+        if (text == null || text.isEmpty()) return false;
+        String lowerText = text.toLowerCase().trim();
+
+        // 步骤1:提取数字长度(用于后续判断)
+        int digitLength = extractDigitLength(lowerText);
+
+        // 步骤2:判断当前消息类型
+        MsgType currentType = getMsgType(lowerText, digitLength);
+
+        // 步骤3:单次内容拦截
+        // - 纯字母/字母数字/纯数字:长度>4
+        // - 数字+文字混合:数字长度>4
+        if ((currentType == MsgType.PURE_LETTER && lowerText.length() > MAX_SINGLE_LEN)
+                || (currentType == MsgType.LETTER_DIGIT && lowerText.length() > MAX_SINGLE_LEN)
+                || (currentType == MsgType.PURE_DIGIT && lowerText.length() > MAX_SINGLE_LEN)
+                || (currentType == MsgType.DIGIT_TEXT_MIX && digitLength > MAX_SINGLE_LEN)) {
+            return true;
+        }
+
+        // 步骤4:连续发送拦截(动态次数)
+        if (isConsecutiveOverLimit(userId, currentType, digitLength)) {
+            return true;
+        }
+
+        // 步骤5:更新会话缓存(存储数字长度)
+        String sessionText = updateAndGetSessionText(userId, lowerText, currentType, digitLength);
+
+        // 步骤6:联系方式检测
+        if (PHONE_PATTERN.matcher(sessionText).find() || WECHAT_PATTERN.matcher(sessionText).find()) {
+            return true;
+        }
+        if (isSensitiveWithAccount(sessionText)) {
+            return true;
+        }
+
+        return false;
+    }
+
+
+    // -------------------------- 辅助方法 --------------------------
+    /**
+     * 判断消息类型(新增数字长度校验)
+     */
+    private static MsgType getMsgType(String text, int digitLength) {
+        if (text.matches("[a-zA-Z]+")) {
+            return MsgType.PURE_LETTER;
+        } else if (text.matches("\\d+")) {
+            return MsgType.PURE_DIGIT;
+        } else if (text.matches(".*[a-zA-Z].*") && text.matches(".*\\d.*")) {
+            return MsgType.LETTER_DIGIT;
+        } else if (digitLength > 0 && text.matches(".*[^a-zA-Z0-9].*")) {
+            // 包含数字 + 包含非字母数字(如中文)→ 数字+文字混合
+            return MsgType.DIGIT_TEXT_MIX;
+        } else {
+            return MsgType.OTHER;
+        }
+    }
+
+    /**
+     * 提取消息中的数字长度
+     */
+    private static int extractDigitLength(String text) {
+        int digitLen = 0;
+        for (char c : text.toCharArray()) {
+            if (Character.isDigit(c)) {
+                digitLen++;
+            }
+        }
+        return digitLen;
+    }
+
+    /**
+     * 动态连续发送拦截:
+     * - DIGIT_TEXT_MIX且数字长度≤3 → 连续3条拦截
+     * - DIGIT_TEXT_MIX且数字长度=4/其他非OTHER类型 → 连续2条拦截
+     */
+    private static boolean isConsecutiveOverLimit(String userId, MsgType currentType, int currentDigitLen) {
+        if (currentType == MsgType.OTHER) {
+            return false;
+        }
+
+        Deque<MsgInfo> sessionMsgs = USER_SESSION_CACHE.get(userId);
+        if (sessionMsgs == null || sessionMsgs.isEmpty()) {
+            return false;
+        }
+
+        // 确定当前类型的拦截阈值
+        int threshold;
+        if (currentType == MsgType.DIGIT_TEXT_MIX && currentDigitLen <= 3) {
+            threshold = CONSECUTIVE_TIMES_LOW; // 数字≤3 → 3条拦截
+        } else {
+            threshold = CONSECUTIVE_TIMES_HIGH; // 数字=4/其他类型 → 2条拦截
+        }
+
+        int consecutiveCount = 1; // 本次算1次
+        Iterator<MsgInfo> iterator = sessionMsgs.descendingIterator();
+        while (iterator.hasNext()) {
+            MsgInfo prevMsg = iterator.next();
+            // 只统计非OTHER类型(且DIGIT_TEXT_MIX需是同类型)
+            if (prevMsg.type != MsgType.OTHER) {
+                consecutiveCount++;
+                if (consecutiveCount >= threshold) {
+                    return true;
+                }
+            } else {
+                break; // 遇到OTHER类型,停止统计
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 更新会话缓存(存储数字长度)
+     */
+    private static String updateAndGetSessionText(String userId, String content, MsgType type, int digitLength) {
+        Deque<MsgInfo> sessionMsgs = USER_SESSION_CACHE.computeIfAbsent(userId, k -> new LinkedList<>());
+        sessionMsgs.addLast(new MsgInfo(content, type, digitLength));
+        if (sessionMsgs.size() > MAX_SESSION_MSG_COUNT) {
+            sessionMsgs.removeFirst();
+        }
+        StringBuilder sb = new StringBuilder();
+        for (MsgInfo msg : sessionMsgs) {
+            sb.append(msg.content);
+        }
+        return sb.toString();
+    }
+
+    /**
+     * 匹配敏感关键词+疑似账号
+     */
+    private static boolean isSensitiveWithAccount(String text) {
+        for (String keyword : SENSITIVE_KEYWORDS) {
+            if (text.contains(keyword)) {
+                Pattern accountPattern = Pattern.compile("[a-zA-Z0-9]{6,}|\\d{4,}");
+                if (accountPattern.matcher(text).find()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+
+    // -------------------------- 测试(动态拦截规则) --------------------------
+    public static void main(String[] args) {
+        String userId = "testUser001";
+        USER_SESSION_CACHE.clear();
+
+        // 测试:数字+文字混合(单条数字长度=1,连续3条拦截)
+        System.out.println("第1次:1吃了没 → " + hasContact(userId, "1吃了没"));    // false(连续1次)
+        System.out.println("第2次:6睡了没 → " + hasContact(userId, "6睡了没"));    // false(连续2次)
+        System.out.println("第3次:6死了没 → " + hasContact(userId, "6死了没"));    // true(连续3次,拦截)
+
+        // 重置缓存,测试数字长度=4的情况
+        USER_SESSION_CACHE.clear();
+        System.out.println("--- 重置缓存 ---");
+        System.out.println("第1次:1234吃了没 → " + hasContact(userId, "1234吃了没")); // false(连续1次)
+        System.out.println("第2次:5678睡了没 → " + hasContact(userId, "5678睡了没")); // true(连续2次,拦截)
+
+        // 测试单条数字长度>4
+        System.out.println("单条数字超4:12345吃了没 → " + hasContact(userId, "12345吃了没")); // true(直接拦截)
+    }
+}