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

红娘用户间消息存储

mazhenhang пре 4 недеља
родитељ
комит
0ae5158168

+ 27 - 6
LiangZhiYUMao/pages/message/chat.vue

@@ -1286,11 +1286,11 @@ export default {
       try {
         console.log('🔄 同步消息到MySQL...', timMessage.ID);
         
-        // 构建同步参数
+        // 构建同步参数(V2版本,支持红娘消息)
         const syncData = {
           messageId: timMessage.ID,
-          fromUserId: timMessage.from,
-          toUserId: timMessage.to,
+          fromTimUserId: String(timMessage.from),  // 保持字符串格式,支持 m_xxx
+          toTimUserId: String(timMessage.to),      // 保持字符串格式,支持 m_xxx
           messageType: this.getMessageType(timMessage),
           content: this.getMessageContent(timMessage),
           sendTime: timMessage.time  // TIM返回的是秒级时间戳
@@ -1324,9 +1324,9 @@ export default {
           }
         }
         
-        // 调用后端同步接口
+        // 调用后端同步接口(V2版本,支持红娘消息)
         const res = await uni.request({
-          url: 'http://localhost:8083/api/chat/syncTIMMessage',
+          url: 'http://localhost:8083/api/chat/syncTIMMessageV2',
           method: 'POST',
           data: syncData,
           header: {
@@ -1335,7 +1335,8 @@ export default {
         });
         
         if (res[1].data.code === 200) {
-          console.log('✅ 消息已同步到MySQL:', timMessage.ID);
+          const msgType = res[1].data.type === 'matchmaker' ? '红娘消息' : '用户消息';
+          console.log(`✅ 消息已同步到MySQL (${msgType}):`, timMessage.ID);
         } else {
           console.warn('⚠️ 消息同步失败:', res[1].data.message);
         }
@@ -1403,6 +1404,26 @@ export default {
       }
     },
     
+    /**
+     * 根据语音时长计算语音消息气泡宽度
+     * @param {number} duration 语音时长(秒)
+     * @returns {string} 宽度样式值
+     */
+    getVoiceWidth(duration) {
+      // 基础宽度 120rpx,每秒增加 10rpx,最大 300rpx
+      const baseWidth = 120;
+      const perSecondWidth = 10;
+      const maxWidth = 300;
+      const minWidth = 120;
+      
+      const seconds = parseInt(duration) || 0;
+      let width = baseWidth + seconds * perSecondWidth;
+      
+      // 限制在最小和最大范围内
+      width = Math.max(minWidth, Math.min(maxWidth, width));
+      
+      return width + 'rpx';
+    },
     
     /**
      * 滚动到底部

+ 23 - 2
LiangZhiYUMao/pages/message/index.vue

@@ -115,7 +115,10 @@
               <!-- 会话内容 -->
               <view class="conversation-content">
                 <view class="content-top">
-                  <text class="user-name">{{ conv.targetUserName }}</text>
+                  <view class="user-info">
+                    <text class="user-name">{{ conv.targetUserName }}</text>
+                    <text v-if="conv.isMatchmaker" class="matchmaker-tag">红娘</text>
+                  </view>
                   <text class="last-time">{{formatTime(conv.lastMessageTime) }}</text>
                 </view>
                 <view class="content-bottom">
@@ -831,6 +834,9 @@ export default {
       // 检查是否在本地置顶列表中
       const isPinned = this.pinnedConversations.includes(timConv.conversationID) ? 1 : 0;
       
+      // 判断是否是红娘:ID 以 m_ 开头
+      const isMatchmaker = targetUserId.startsWith('m_');
+      
       return {
         id: timConv.conversationID,
         userId: this.userId,
@@ -842,7 +848,8 @@ export default {
         lastMessageTime: timConv.lastMessage?.lastTime ? timConv.lastMessage.lastTime * 1000 : Date.now(),
         unreadCount: timConv.unreadCount || 0,
         isOnline: false,
-        isPinned: isPinned
+        isPinned: isPinned,
+        isMatchmaker: isMatchmaker
       };
     },
     
@@ -1697,12 +1704,26 @@ export default {
       justify-content: space-between;
       margin-bottom: 12rpx;
       
+      .user-info {
+        display: flex;
+        align-items: center;
+      }
+      
       .user-name {
         font-size: 32rpx;
         font-weight: 500;
         color: #333333;
       }
       
+      .matchmaker-tag {
+        background: linear-gradient(135deg, #ff6b6b, #ee5a24);
+        color: #fff;
+        font-size: 20rpx;
+        padding: 4rpx 12rpx;
+        border-radius: 6rpx;
+        margin-left: 10rpx;
+      }
+      
       .last-time {
         font-size: 24rpx;
         color: #999999;

+ 7 - 5
LiangZhiYUMao/utils/tim-manager.js

@@ -146,10 +146,11 @@ class TIMManager {
         }
       };
       
+      // 使用V2接口,支持红娘消息(TIM用户ID可能是 m_xxx 格式)
       const syncData = {
         messageId: timMessage.ID,
-        fromUserId: timMessage.from,
-        toUserId: timMessage.to,
+        fromTimUserId: String(timMessage.from),  // 保持字符串格式,支持 m_xxx
+        toTimUserId: String(timMessage.to),      // 保持字符串格式,支持 m_xxx
         messageType: getMessageType(timMessage),
         content: getMessageContent(timMessage),
         sendTime: timMessage.time
@@ -183,9 +184,9 @@ class TIMManager {
         syncData.thumbnailUrl = imageInfo.imageUrl;
       }
       
-      // 调用后端同步接口
+      // 调用后端同步接口(V2版本,支持红娘消息)
       const res = await uni.request({
-        url: 'http://localhost:8083/api/chat/syncTIMMessage',
+        url: 'http://localhost:8083/api/chat/syncTIMMessageV2',
         method: 'POST',
         data: syncData,
         header: {
@@ -194,7 +195,8 @@ class TIMManager {
       });
       
       if (res[1].data.code === 200) {
-        console.log('✅ 接收消息已同步到MySQL:', timMessage.ID);
+        const msgType = res[1].data.type === 'matchmaker' ? '红娘消息' : '用户消息';
+        console.log(`✅ 接收消息已同步到MySQL (${msgType}):`, timMessage.ID);
       }
     } catch (error) {
       console.error('❌ 同步接收消息失败:', error);

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

@@ -3,7 +3,9 @@ package com.zhentao.controller;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.zhentao.entity.ChatConversation;
 import com.zhentao.entity.ChatMessage;
+import com.zhentao.entity.MatchmakerChatMessage;
 import com.zhentao.service.ChatMessageService;
+import com.zhentao.service.MatchmakerChatMessageService;
 import com.zhentao.service.OnlineUserService;
 import com.zhentao.service.UserVipService;
 import com.zhentao.utils.ContactFilter;
@@ -34,6 +36,10 @@ public class ChatController {
 
     @Autowired
     private UserVipService userVipService;
+    
+    @Autowired
+    private MatchmakerChatMessageService matchmakerChatMessageService;
+    
     @GetMapping("/test")
     public String test() {
         return "ChatController is working!";
@@ -356,6 +362,198 @@ public class ChatController {
         return result;
     }
     
+    /**
+     * 同步TIM消息到MySQL(支持红娘消息)
+     * POST /api/chat/syncTIMMessageV2
+     * Body: {
+     *   "messageId": "xxx",
+     *   "fromTimUserId": "m_123" 或 "456",  // TIM用户ID,红娘以m_开头
+     *   "toTimUserId": "456" 或 "m_123",    // TIM用户ID
+     *   "messageType": 1,
+     *   "content": "消息内容",
+     *   "sendTime": 1234567890,
+     *   "mediaUrl": "xxx" (可选)
+     * }
+     */
+    @PostMapping("/syncTIMMessageV2")
+    public Map<String, Object> syncTIMMessageV2(@RequestBody Map<String, Object> params) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            System.out.println("🔄 开始同步TIM消息到MySQL(V2版本)...");
+            
+            String messageId = params.get("messageId").toString();
+            String fromTimUserId = params.get("fromTimUserId").toString();
+            String toTimUserId = params.get("toTimUserId").toString();
+            Integer messageType = Integer.valueOf(params.get("messageType").toString());
+            String content = params.get("content") != null ? params.get("content").toString() : "";
+            String mediaUrl = params.containsKey("mediaUrl") ? params.get("mediaUrl").toString() : null;
+            
+            // 解析发送时间
+            java.util.Date sendTime;
+            if (params.containsKey("sendTime")) {
+                long sendTimeMs = Long.parseLong(params.get("sendTime").toString());
+                // TIM返回的是秒级时间戳,需要转换为毫秒
+                if (sendTimeMs < 10000000000L) {
+                    sendTimeMs = sendTimeMs * 1000;
+                }
+                sendTime = new java.util.Date(sendTimeMs);
+            } else {
+                sendTime = new java.util.Date();
+            }
+            
+            // 判断是否是红娘相关消息
+            boolean isFromMatchmaker = MatchmakerChatMessage.isMatchmaker(fromTimUserId);
+            boolean isToMatchmaker = MatchmakerChatMessage.isMatchmaker(toTimUserId);
+            
+            if (isFromMatchmaker || isToMatchmaker) {
+                // 红娘消息,保存到 matchmaker_chat_message 表
+                System.out.println("📌 检测到红娘消息,保存到红娘消息表...");
+                
+                MatchmakerChatMessage matchmakerMessage = matchmakerChatMessageService.saveFromTimMessage(
+                    fromTimUserId, toTimUserId, messageType, content, mediaUrl, messageId
+                );
+                
+                if (matchmakerMessage != null) {
+                    // 更新发送时间
+                    matchmakerMessage.setSendTime(sendTime);
+                    
+                    System.out.println("✅ 红娘消息已同步: " + messageId);
+                    result.put("code", 200);
+                    result.put("message", "红娘消息同步成功");
+                    result.put("data", messageId);
+                    result.put("type", "matchmaker");
+                } else {
+                    result.put("code", 400);
+                    result.put("message", "无法解析红娘消息");
+                }
+            } else {
+                // 普通用户消息,保存到 chat_message 表
+                System.out.println("📌 普通用户消息,保存到用户消息表...");
+                
+                Long fromUserId = Long.valueOf(fromTimUserId);
+                Long toUserId = Long.valueOf(toTimUserId);
+                
+                // 检查消息是否已存在
+                ChatMessage existingMessage = chatMessageService.getMessageByMessageId(messageId);
+                if (existingMessage != null) {
+                    System.out.println("⚠️ 消息已存在,跳过: " + messageId);
+                    result.put("code", 200);
+                    result.put("message", "消息已存在");
+                    result.put("data", null);
+                    return result;
+                }
+                
+                // 构建ChatMessage实体
+                ChatMessage chatMessage = new ChatMessage();
+                chatMessage.setMessageId(messageId);
+                chatMessage.setFromUserId(fromUserId);
+                chatMessage.setToUserId(toUserId);
+                chatMessage.setMessageType(messageType);
+                chatMessage.setContent(content);
+                chatMessage.setMediaUrl(mediaUrl);
+                chatMessage.setSendTime(sendTime);
+                chatMessage.setSendStatus(2); // 已送达
+                chatMessage.setDeliverTime(new java.util.Date());
+                
+                // 会话ID
+                String conversationId = fromUserId < toUserId ? 
+                    fromUserId + "_" + toUserId : toUserId + "_" + fromUserId;
+                chatMessage.setConversationId(conversationId);
+                
+                // 设置创建时间和更新时间
+                java.util.Date now = new java.util.Date();
+                chatMessage.setCreateTime(now);
+                chatMessage.setUpdateTime(now);
+                
+                // 保存到MySQL
+                chatMessageService.saveChatMessage(chatMessage);
+                
+                // 更新会话表
+                chatMessageService.updateOrCreateConversation(
+                    fromUserId, toUserId, content, messageType, sendTime
+                );
+                
+                System.out.println("✅ 用户消息已同步: " + messageId);
+                result.put("code", 200);
+                result.put("message", "用户消息同步成功");
+                result.put("data", messageId);
+                result.put("type", "user");
+            }
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ 同步TIM消息失败: " + e.getMessage());
+            result.put("code", 500);
+            result.put("message", "同步失败: " + e.getMessage());
+        }
+        
+        return result;
+    }
+    
+    /**
+     * 获取红娘与用户的聊天记录
+     * GET /api/chat/matchmakerMessages?matchmakerId=123&userId=456&page=0&size=20
+     */
+    @GetMapping("/matchmakerMessages")
+    public Map<String, Object> getMatchmakerMessages(
+            @RequestParam Long matchmakerId,
+            @RequestParam Long userId,
+            @RequestParam(defaultValue = "0") int page,
+            @RequestParam(defaultValue = "20") int size) {
+        
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            IPage<MatchmakerChatMessage> messages = matchmakerChatMessageService.getConversationMessages(
+                matchmakerId, userId, page, size
+            );
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", messages.getRecords());
+            result.put("total", messages.getTotal());
+            result.put("pages", messages.getPages());
+            result.put("current", messages.getCurrent());
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            result.put("code", 500);
+            result.put("message", "查询失败: " + e.getMessage());
+        }
+        
+        return result;
+    }
+    
+    /**
+     * 标记红娘消息为已读
+     * POST /api/chat/markMatchmakerMessagesRead
+     * Body: {"matchmakerId": 123, "userId": 456, "readerType": 1}
+     * readerType: 1-用户阅读 2-红娘阅读
+     */
+    @PostMapping("/markMatchmakerMessagesRead")
+    public Map<String, Object> markMatchmakerMessagesRead(@RequestBody Map<String, Object> params) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            Long matchmakerId = Long.valueOf(params.get("matchmakerId").toString());
+            Long userId = Long.valueOf(params.get("userId").toString());
+            Integer readerType = Integer.valueOf(params.get("readerType").toString());
+            
+            matchmakerChatMessageService.markMessagesAsRead(matchmakerId, userId, readerType);
+            
+            result.put("code", 200);
+            result.put("message", "标记成功");
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            result.put("code", 500);
+            result.put("message", "标记失败: " + e.getMessage());
+        }
+        
+        return result;
+    }
+    
     /**
      * 健康检查
      * GET /api/chat/health

+ 172 - 0
service/websocket/src/main/java/com/zhentao/entity/MatchmakerChatMessage.java

@@ -0,0 +1,172 @@
+package com.zhentao.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 红娘与用户聊天消息实体类
+ * 存储在MySQL中
+ */
+@Data
+@TableName("matchmaker_chat_message")
+public class MatchmakerChatMessage implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 消息主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 消息唯一ID(UUID)
+     */
+    private String messageId;
+
+    /**
+     * 会话ID(格式:m_红娘ID_用户ID)
+     */
+    private String conversationId;
+
+    /**
+     * 红娘ID
+     */
+    private Long matchmakerId;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 发送方类型:1-用户发送 2-红娘发送
+     */
+    private Integer senderType;
+
+    /**
+     * 消息类型:1-文本 2-图片 3-语音 4-视频 5-文件
+     */
+    private Integer messageType;
+
+    /**
+     * 消息内容(文本内容或媒体描述)
+     */
+    private String content;
+
+    /**
+     * 媒体文件URL
+     */
+    private String mediaUrl;
+
+    /**
+     * 缩略图URL
+     */
+    private String thumbnailUrl;
+
+    /**
+     * 文件大小(字节)
+     */
+    private Long mediaSize;
+
+    /**
+     * 语音/视频时长(秒)
+     */
+    private Integer duration;
+
+    /**
+     * 发送状态:1-发送中 2-已送达 3-已读 4-失败
+     */
+    private Integer sendStatus;
+
+    /**
+     * 发送时间
+     */
+    private Date sendTime;
+
+    /**
+     * 送达时间
+     */
+    private Date deliverTime;
+
+    /**
+     * 阅读时间
+     */
+    private Date readTime;
+
+    /**
+     * 是否已撤回(0:否 1:是)
+     */
+    private Integer isRecalled;
+
+    /**
+     * 撤回时间
+     */
+    private Date recallTime;
+
+    /**
+     * 额外数据(JSON格式字符串)
+     */
+    private String extraData;
+
+    /**
+     * 是否删除
+     */
+    @TableLogic
+    private Integer isDeleted;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+
+    /**
+     * 发送方类型常量
+     */
+    public static final int SENDER_TYPE_USER = 1;       // 用户发送
+    public static final int SENDER_TYPE_MATCHMAKER = 2; // 红娘发送
+
+    /**
+     * 生成会话ID
+     * 格式:m_红娘ID_用户ID
+     */
+    public static String generateConversationId(Long matchmakerId, Long userId) {
+        return "m_" + matchmakerId + "_" + userId;
+    }
+
+    /**
+     * 从TIM会话ID解析红娘ID
+     * TIM会话ID格式:C2Cm_红娘ID 或 m_红娘ID
+     */
+    public static Long parseMatchmakerIdFromTimId(String timUserId) {
+        if (timUserId == null) return null;
+        String id = timUserId.replace("C2C", "");
+        if (id.startsWith("m_")) {
+            try {
+                return Long.parseLong(id.substring(2));
+            } catch (NumberFormatException e) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 判断TIM用户ID是否是红娘
+     */
+    public static boolean isMatchmaker(String timUserId) {
+        if (timUserId == null) return false;
+        String id = timUserId.replace("C2C", "");
+        return id.startsWith("m_");
+    }
+}

+ 172 - 0
service/websocket/src/main/java/com/zhentao/entity/MatchmakerChatMessageMongo.java

@@ -0,0 +1,172 @@
+package com.zhentao.entity;
+
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.CompoundIndex;
+import org.springframework.data.mongodb.core.index.CompoundIndexes;
+import org.springframework.data.mongodb.core.index.Indexed;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+
+import java.util.Date;
+
+/**
+ * MongoDB红娘与用户聊天消息实体(NoSQL存储)
+ * 用于大数据分析、历史存档、数据挖掘
+ */
+@Data
+@Document(collection = "matchmaker_chat_messages")
+@CompoundIndexes({
+    @CompoundIndex(name = "idx_conversation_time", def = "{'conversation_id': 1, 'send_time': -1}"),
+    @CompoundIndex(name = "idx_matchmaker_user", def = "{'matchmaker_id': 1, 'user_id': 1}")
+})
+public class MatchmakerChatMessageMongo {
+    
+    /**
+     * MongoDB主键(自动生成)
+     */
+    @Id
+    private String id;
+    
+    /**
+     * 消息ID(来自腾讯云IM)
+     */
+    @Field("message_id")
+    @Indexed(unique = true)
+    private String messageId;
+    
+    /**
+     * 会话ID(格式:m_红娘ID_用户ID)
+     */
+    @Field("conversation_id")
+    @Indexed
+    private String conversationId;
+    
+    /**
+     * 红娘ID
+     */
+    @Field("matchmaker_id")
+    @Indexed
+    private Long matchmakerId;
+    
+    /**
+     * 用户ID
+     */
+    @Field("user_id")
+    @Indexed
+    private Long userId;
+    
+    /**
+     * 发送方类型:1-用户发送 2-红娘发送
+     */
+    @Field("sender_type")
+    private Integer senderType;
+    
+    /**
+     * 消息类型
+     * 1: TEXT-文本消息
+     * 2: IMAGE-图片消息
+     * 3: VOICE-语音消息
+     * 4: VIDEO-视频消息
+     * 5: FILE-文件消息
+     */
+    @Field("message_type")
+    private Integer messageType;
+    
+    /**
+     * 消息内容
+     */
+    @Field("content")
+    private String content;
+    
+    /**
+     * 媒体文件URL(图片、语音、视频、文件)
+     */
+    @Field("media_url")
+    private String mediaUrl;
+    
+    /**
+     * 缩略图URL(图片、视频)
+     */
+    @Field("thumbnail_url")
+    private String thumbnailUrl;
+    
+    /**
+     * 媒体文件大小(字节)
+     */
+    @Field("media_size")
+    private Long mediaSize;
+    
+    /**
+     * 媒体时长(秒,用于语音和视频)
+     */
+    @Field("duration")
+    private Integer duration;
+    
+    /**
+     * 发送状态
+     * 1: SENDING-发送中
+     * 2: DELIVERED-已送达
+     * 3: READ-已读
+     * 4: FAILED-发送失败
+     */
+    @Field("send_status")
+    private Integer sendStatus;
+    
+    /**
+     * 发送时间
+     */
+    @Field("send_time")
+    @Indexed
+    private Date sendTime;
+    
+    /**
+     * 送达时间
+     */
+    @Field("deliver_time")
+    private Date deliverTime;
+    
+    /**
+     * 已读时间
+     */
+    @Field("read_time")
+    private Date readTime;
+    
+    /**
+     * 是否已撤回
+     */
+    @Field("is_recalled")
+    private Integer isRecalled;
+    
+    /**
+     * 撤回时间
+     */
+    @Field("recall_time")
+    private Date recallTime;
+    
+    /**
+     * 消息来源
+     * TIM: 腾讯云IM同步
+     * MANUAL: 手动创建
+     */
+    @Field("source")
+    private String source;
+    
+    /**
+     * 创建时间
+     */
+    @Field("created_at")
+    private Date createdAt;
+    
+    /**
+     * 更新时间
+     */
+    @Field("updated_at")
+    private Date updatedAt;
+    
+    /**
+     * 扩展字段(JSON格式,用于存储其他信息)
+     */
+    @Field("extra_data")
+    private String extraData;
+}

+ 89 - 0
service/websocket/src/main/java/com/zhentao/repository/MatchmakerChatMessageMapper.java

@@ -0,0 +1,89 @@
+package com.zhentao.repository;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zhentao.entity.MatchmakerChatMessage;
+import org.apache.ibatis.annotations.*;
+
+import java.util.List;
+
+/**
+ * 红娘与用户聊天消息Mapper
+ */
+@Mapper
+public interface MatchmakerChatMessageMapper extends BaseMapper<MatchmakerChatMessage> {
+
+    /**
+     * 根据会话ID分页查询消息
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE conversation_id = #{conversationId} AND is_deleted = 0 ORDER BY send_time DESC")
+    IPage<MatchmakerChatMessage> selectByConversationId(Page<MatchmakerChatMessage> page, @Param("conversationId") String conversationId);
+
+    /**
+     * 根据消息ID查询
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE message_id = #{messageId} AND is_deleted = 0")
+    MatchmakerChatMessage selectByMessageId(@Param("messageId") String messageId);
+
+    /**
+     * 查询红娘的所有会话消息(分页)
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE matchmaker_id = #{matchmakerId} AND is_deleted = 0 ORDER BY send_time DESC")
+    IPage<MatchmakerChatMessage> selectByMatchmakerId(Page<MatchmakerChatMessage> page, @Param("matchmakerId") Long matchmakerId);
+
+    /**
+     * 查询用户与红娘的所有会话消息(分页)
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE user_id = #{userId} AND is_deleted = 0 ORDER BY send_time DESC")
+    IPage<MatchmakerChatMessage> selectByUserId(Page<MatchmakerChatMessage> page, @Param("userId") Long userId);
+
+    /**
+     * 查询红娘与特定用户的聊天记录(分页)
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE matchmaker_id = #{matchmakerId} AND user_id = #{userId} AND is_deleted = 0 ORDER BY send_time DESC")
+    IPage<MatchmakerChatMessage> selectByMatchmakerAndUser(Page<MatchmakerChatMessage> page, 
+                                                           @Param("matchmakerId") Long matchmakerId, 
+                                                           @Param("userId") Long userId);
+
+    /**
+     * 查询离线消息(用户未读的消息)
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE user_id = #{userId} AND sender_type = 2 AND send_status < 3 AND is_deleted = 0 ORDER BY send_time ASC LIMIT #{limit}")
+    List<MatchmakerChatMessage> selectOfflineMessagesForUser(@Param("userId") Long userId, @Param("limit") int limit);
+
+    /**
+     * 查询离线消息(红娘未读的消息)
+     */
+    @Select("SELECT * FROM matchmaker_chat_message WHERE matchmaker_id = #{matchmakerId} AND sender_type = 1 AND send_status < 3 AND is_deleted = 0 ORDER BY send_time ASC LIMIT #{limit}")
+    List<MatchmakerChatMessage> selectOfflineMessagesForMatchmaker(@Param("matchmakerId") Long matchmakerId, @Param("limit") int limit);
+
+    /**
+     * 更新消息为已读
+     */
+    @Update("UPDATE matchmaker_chat_message SET send_status = 3, read_time = NOW(), update_time = NOW() " +
+            "WHERE matchmaker_id = #{matchmakerId} AND user_id = #{userId} AND sender_type = #{senderType} AND send_status < 3 AND is_deleted = 0")
+    int updateMessagesToRead(@Param("matchmakerId") Long matchmakerId, 
+                             @Param("userId") Long userId, 
+                             @Param("senderType") Integer senderType);
+
+    /**
+     * 撤回消息
+     */
+    @Update("UPDATE matchmaker_chat_message SET is_recalled = 1, recall_time = NOW(), update_time = NOW() WHERE message_id = #{messageId}")
+    int recallMessage(@Param("messageId") String messageId);
+
+    /**
+     * 统计红娘今日消息数
+     */
+    @Select("SELECT COUNT(*) FROM matchmaker_chat_message WHERE matchmaker_id = #{matchmakerId} AND sender_type = 2 AND DATE(send_time) = CURDATE() AND is_deleted = 0")
+    int countMatchmakerTodayMessages(@Param("matchmakerId") Long matchmakerId);
+
+    /**
+     * 统计红娘与用户的未读消息数
+     */
+    @Select("SELECT COUNT(*) FROM matchmaker_chat_message WHERE matchmaker_id = #{matchmakerId} AND user_id = #{userId} AND sender_type = #{senderType} AND send_status < 3 AND is_deleted = 0")
+    int countUnreadMessages(@Param("matchmakerId") Long matchmakerId, 
+                            @Param("userId") Long userId, 
+                            @Param("senderType") Integer senderType);
+}

+ 76 - 0
service/websocket/src/main/java/com/zhentao/repository/MatchmakerChatMessageMongoRepository.java

@@ -0,0 +1,76 @@
+package com.zhentao.repository;
+
+import com.zhentao.entity.MatchmakerChatMessageMongo;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.mongodb.repository.MongoRepository;
+import org.springframework.data.mongodb.repository.Query;
+import org.springframework.stereotype.Repository;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 红娘与用户聊天消息MongoDB仓库
+ */
+@Repository
+public interface MatchmakerChatMessageMongoRepository extends MongoRepository<MatchmakerChatMessageMongo, String> {
+
+    /**
+     * 根据消息ID查询
+     */
+    Optional<MatchmakerChatMessageMongo> findByMessageId(String messageId);
+
+    /**
+     * 根据会话ID查询消息(分页,按时间倒序)
+     */
+    Page<MatchmakerChatMessageMongo> findByConversationIdOrderBySendTimeDesc(String conversationId, Pageable pageable);
+
+    /**
+     * 根据红娘ID查询所有消息(分页)
+     */
+    Page<MatchmakerChatMessageMongo> findByMatchmakerIdOrderBySendTimeDesc(Long matchmakerId, Pageable pageable);
+
+    /**
+     * 根据用户ID查询所有消息(分页)
+     */
+    Page<MatchmakerChatMessageMongo> findByUserIdOrderBySendTimeDesc(Long userId, Pageable pageable);
+
+    /**
+     * 根据红娘ID和用户ID查询消息(分页)
+     */
+    Page<MatchmakerChatMessageMongo> findByMatchmakerIdAndUserIdOrderBySendTimeDesc(Long matchmakerId, Long userId, Pageable pageable);
+
+    /**
+     * 查询某时间段内的消息
+     */
+    @Query("{'conversation_id': ?0, 'send_time': {$gte: ?1, $lte: ?2}}")
+    List<MatchmakerChatMessageMongo> findByConversationIdAndSendTimeBetween(String conversationId, Date startTime, Date endTime);
+
+    /**
+     * 统计红娘的消息数量
+     */
+    long countByMatchmakerIdAndSenderType(Long matchmakerId, Integer senderType);
+
+    /**
+     * 统计用户的消息数量
+     */
+    long countByUserIdAndSenderType(Long userId, Integer senderType);
+
+    /**
+     * 查询未读消息
+     */
+    List<MatchmakerChatMessageMongo> findByMatchmakerIdAndUserIdAndSenderTypeAndSendStatusLessThan(
+            Long matchmakerId, Long userId, Integer senderType, Integer sendStatus);
+
+    /**
+     * 根据消息ID删除
+     */
+    void deleteByMessageId(String messageId);
+
+    /**
+     * 检查消息是否存在
+     */
+    boolean existsByMessageId(String messageId);
+}

+ 320 - 0
service/websocket/src/main/java/com/zhentao/service/MatchmakerChatMessageService.java

@@ -0,0 +1,320 @@
+package com.zhentao.service;
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zhentao.entity.MatchmakerChatMessage;
+import com.zhentao.entity.MatchmakerChatMessageMongo;
+import com.zhentao.repository.MatchmakerChatMessageMapper;
+import com.zhentao.repository.MatchmakerChatMessageMongoRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.UUID;
+
+/**
+ * 红娘与用户聊天消息服务
+ * 同时存储到MySQL和MongoDB
+ */
+@Service
+public class MatchmakerChatMessageService {
+
+    @Autowired
+    private MatchmakerChatMessageMapper matchmakerChatMessageMapper;
+
+    @Autowired(required = false)
+    private MatchmakerChatMessageMongoRepository mongoRepository;
+
+    /**
+     * 保存红娘与用户的聊天消息
+     * 同时存储到MySQL和MongoDB
+     * 
+     * @param matchmakerId 红娘ID
+     * @param userId 用户ID
+     * @param senderType 发送方类型:1-用户发送 2-红娘发送
+     * @param messageType 消息类型:1-文本 2-图片 3-语音 4-视频 5-文件
+     * @param content 消息内容
+     * @param mediaUrl 媒体文件URL(可选)
+     * @param timMessageId TIM消息ID(可选,如果没有则自动生成)
+     * @return 保存的消息实体
+     */
+    public MatchmakerChatMessage saveMessage(Long matchmakerId, Long userId, Integer senderType,
+                                              Integer messageType, String content, String mediaUrl,
+                                              String timMessageId) {
+        // 构建消息实体
+        MatchmakerChatMessage message = new MatchmakerChatMessage();
+        message.setMessageId(timMessageId != null ? timMessageId : UUID.randomUUID().toString());
+        message.setConversationId(MatchmakerChatMessage.generateConversationId(matchmakerId, userId));
+        message.setMatchmakerId(matchmakerId);
+        message.setUserId(userId);
+        message.setSenderType(senderType);
+        message.setMessageType(messageType);
+        message.setContent(content);
+        message.setMediaUrl(mediaUrl);
+        message.setSendStatus(1); // 发送中
+        message.setSendTime(new Date());
+        message.setIsRecalled(0);
+        message.setIsDeleted(0);
+        message.setCreateTime(new Date());
+        message.setUpdateTime(new Date());
+
+        // 保存到MySQL和MongoDB
+        return saveMessage(message);
+    }
+
+    /**
+     * 保存消息(完整实体)
+     * 同时存储到MySQL和MongoDB
+     * 
+     * @param message 消息实体
+     * @return 保存的消息实体
+     */
+    public MatchmakerChatMessage saveMessage(MatchmakerChatMessage message) {
+        try {
+            // 1. 保存到MySQL
+            matchmakerChatMessageMapper.insert(message);
+            System.out.println("✅ 红娘消息已保存到MySQL: " + message.getMessageId());
+
+            // 2. 同时保存到MongoDB
+            saveToMongoDB(message);
+
+            return message;
+        } catch (DuplicateKeyException e) {
+            // 消息已存在,忽略重复插入(幂等性保证)
+            System.out.println("⚠️ 红娘消息已存在,跳过保存: " + message.getMessageId());
+            return matchmakerChatMessageMapper.selectByMessageId(message.getMessageId());
+        } catch (Exception e) {
+            System.err.println("❌ 保存红娘消息失败: " + message.getMessageId() + ", 错误: " + e.getMessage());
+            throw e;
+        }
+    }
+
+    /**
+     * 保存消息到MongoDB
+     */
+    private void saveToMongoDB(MatchmakerChatMessage message) {
+        if (mongoRepository == null) {
+            System.out.println("⚠️ MongoDB未配置,跳过MongoDB存储");
+            return;
+        }
+
+        try {
+            // 检查是否已存在
+            if (mongoRepository.existsByMessageId(message.getMessageId())) {
+                System.out.println("⚠️ MongoDB中消息已存在,跳过: " + message.getMessageId());
+                return;
+            }
+
+            // 转换为MongoDB实体
+            MatchmakerChatMessageMongo mongoMessage = convertToMongoEntity(message);
+
+            // 保存到MongoDB
+            mongoRepository.save(mongoMessage);
+            System.out.println("✅ 红娘消息已同步到MongoDB: " + message.getMessageId());
+
+        } catch (Exception e) {
+            // MongoDB存储失败不影响主流程
+            System.err.println("⚠️ MongoDB存储失败(不影响主流程): " + e.getMessage());
+        }
+    }
+
+    /**
+     * 转换为MongoDB实体
+     */
+    private MatchmakerChatMessageMongo convertToMongoEntity(MatchmakerChatMessage message) {
+        MatchmakerChatMessageMongo mongoMessage = new MatchmakerChatMessageMongo();
+        mongoMessage.setMessageId(message.getMessageId());
+        mongoMessage.setConversationId(message.getConversationId());
+        mongoMessage.setMatchmakerId(message.getMatchmakerId());
+        mongoMessage.setUserId(message.getUserId());
+        mongoMessage.setSenderType(message.getSenderType());
+        mongoMessage.setMessageType(message.getMessageType());
+        mongoMessage.setContent(message.getContent());
+        mongoMessage.setMediaUrl(message.getMediaUrl());
+        mongoMessage.setThumbnailUrl(message.getThumbnailUrl());
+        mongoMessage.setMediaSize(message.getMediaSize());
+        mongoMessage.setDuration(message.getDuration());
+        mongoMessage.setSendStatus(message.getSendStatus());
+        mongoMessage.setSendTime(message.getSendTime());
+        mongoMessage.setDeliverTime(message.getDeliverTime());
+        mongoMessage.setReadTime(message.getReadTime());
+        mongoMessage.setIsRecalled(message.getIsRecalled());
+        mongoMessage.setRecallTime(message.getRecallTime());
+        mongoMessage.setSource("TIM");
+        mongoMessage.setCreatedAt(new Date());
+        mongoMessage.setUpdatedAt(new Date());
+        mongoMessage.setExtraData(message.getExtraData());
+        return mongoMessage;
+    }
+
+    /**
+     * 从TIM消息同步保存
+     * 根据发送者和接收者ID判断是用户发送还是红娘发送
+     * 
+     * @param fromTimUserId 发送者TIM用户ID(如 "m_123" 或 "456")
+     * @param toTimUserId 接收者TIM用户ID(如 "m_123" 或 "456")
+     * @param messageType 消息类型
+     * @param content 消息内容
+     * @param mediaUrl 媒体URL
+     * @param timMessageId TIM消息ID
+     * @return 保存的消息实体,如果不是红娘相关消息则返回null
+     */
+    public MatchmakerChatMessage saveFromTimMessage(String fromTimUserId, String toTimUserId,
+                                                     Integer messageType, String content,
+                                                     String mediaUrl, String timMessageId) {
+        Long matchmakerId = null;
+        Long userId = null;
+        Integer senderType = null;
+
+        // 判断发送者是否是红娘
+        if (MatchmakerChatMessage.isMatchmaker(fromTimUserId)) {
+            // 红娘发送给用户
+            matchmakerId = MatchmakerChatMessage.parseMatchmakerIdFromTimId(fromTimUserId);
+            try {
+                userId = Long.parseLong(toTimUserId.replace("C2C", ""));
+            } catch (NumberFormatException e) {
+                System.err.println("❌ 无法解析用户ID: " + toTimUserId);
+                return null;
+            }
+            senderType = MatchmakerChatMessage.SENDER_TYPE_MATCHMAKER;
+        } else if (MatchmakerChatMessage.isMatchmaker(toTimUserId)) {
+            // 用户发送给红娘
+            matchmakerId = MatchmakerChatMessage.parseMatchmakerIdFromTimId(toTimUserId);
+            try {
+                userId = Long.parseLong(fromTimUserId.replace("C2C", ""));
+            } catch (NumberFormatException e) {
+                System.err.println("❌ 无法解析用户ID: " + fromTimUserId);
+                return null;
+            }
+            senderType = MatchmakerChatMessage.SENDER_TYPE_USER;
+        } else {
+            // 不是红娘相关消息
+            return null;
+        }
+
+        if (matchmakerId == null || userId == null) {
+            System.err.println("❌ 无法解析红娘或用户ID");
+            return null;
+        }
+
+        // 保存消息
+        return saveMessage(matchmakerId, userId, senderType, messageType, content, mediaUrl, timMessageId);
+    }
+
+    /**
+     * 查询红娘与用户的聊天记录(分页)
+     */
+    public IPage<MatchmakerChatMessage> getConversationMessages(Long matchmakerId, Long userId, int page, int size) {
+        String conversationId = MatchmakerChatMessage.generateConversationId(matchmakerId, userId);
+        Page<MatchmakerChatMessage> pageParam = new Page<>(page + 1, size);
+        return matchmakerChatMessageMapper.selectByConversationId(pageParam, conversationId);
+    }
+
+    /**
+     * 查询红娘的所有聊天记录(分页)
+     */
+    public IPage<MatchmakerChatMessage> getMatchmakerMessages(Long matchmakerId, int page, int size) {
+        Page<MatchmakerChatMessage> pageParam = new Page<>(page + 1, size);
+        return matchmakerChatMessageMapper.selectByMatchmakerId(pageParam, matchmakerId);
+    }
+
+    /**
+     * 查询用户与红娘的所有聊天记录(分页)
+     */
+    public IPage<MatchmakerChatMessage> getUserMessages(Long userId, int page, int size) {
+        Page<MatchmakerChatMessage> pageParam = new Page<>(page + 1, size);
+        return matchmakerChatMessageMapper.selectByUserId(pageParam, userId);
+    }
+
+    /**
+     * 根据消息ID查询
+     */
+    public MatchmakerChatMessage getByMessageId(String messageId) {
+        return matchmakerChatMessageMapper.selectByMessageId(messageId);
+    }
+
+    /**
+     * 标记消息为已读
+     * 
+     * @param matchmakerId 红娘ID
+     * @param userId 用户ID
+     * @param readerType 阅读者类型:1-用户阅读(红娘发送的消息) 2-红娘阅读(用户发送的消息)
+     */
+    public void markMessagesAsRead(Long matchmakerId, Long userId, Integer readerType) {
+        // 如果是用户阅读,则标记红娘发送的消息为已读
+        // 如果是红娘阅读,则标记用户发送的消息为已读
+        Integer senderType = (readerType == 1) ? MatchmakerChatMessage.SENDER_TYPE_MATCHMAKER 
+                                                : MatchmakerChatMessage.SENDER_TYPE_USER;
+        
+        matchmakerChatMessageMapper.updateMessagesToRead(matchmakerId, userId, senderType);
+        System.out.println("✅ 已标记消息为已读: 红娘=" + matchmakerId + ", 用户=" + userId + ", 发送方=" + senderType);
+    }
+
+    /**
+     * 撤回消息
+     */
+    public boolean recallMessage(String messageId, String operatorTimId) {
+        MatchmakerChatMessage message = matchmakerChatMessageMapper.selectByMessageId(messageId);
+        if (message == null) {
+            System.err.println("❌ 消息不存在: " + messageId);
+            return false;
+        }
+
+        // 验证操作者是否是发送者
+        boolean isMatchmaker = MatchmakerChatMessage.isMatchmaker(operatorTimId);
+        if (isMatchmaker) {
+            Long operatorId = MatchmakerChatMessage.parseMatchmakerIdFromTimId(operatorTimId);
+            if (!message.getMatchmakerId().equals(operatorId) || 
+                message.getSenderType() != MatchmakerChatMessage.SENDER_TYPE_MATCHMAKER) {
+                System.err.println("❌ 只能撤回自己发送的消息");
+                return false;
+            }
+        } else {
+            try {
+                Long operatorId = Long.parseLong(operatorTimId.replace("C2C", ""));
+                if (!message.getUserId().equals(operatorId) || 
+                    message.getSenderType() != MatchmakerChatMessage.SENDER_TYPE_USER) {
+                    System.err.println("❌ 只能撤回自己发送的消息");
+                    return false;
+                }
+            } catch (NumberFormatException e) {
+                System.err.println("❌ 无法解析操作者ID: " + operatorTimId);
+                return false;
+            }
+        }
+
+        // 检查时间限制(2分钟内)
+        long diff = System.currentTimeMillis() - message.getSendTime().getTime();
+        if (diff > 2 * 60 * 1000) {
+            System.err.println("❌ 超过撤回时间限制");
+            return false;
+        }
+
+        // 执行撤回
+        matchmakerChatMessageMapper.recallMessage(messageId);
+        System.out.println("✅ 消息已撤回: " + messageId);
+        return true;
+    }
+
+    /**
+     * 统计红娘今日消息数
+     */
+    public int countMatchmakerTodayMessages(Long matchmakerId) {
+        return matchmakerChatMessageMapper.countMatchmakerTodayMessages(matchmakerId);
+    }
+
+    /**
+     * 统计未读消息数
+     * 
+     * @param matchmakerId 红娘ID
+     * @param userId 用户ID
+     * @param readerType 阅读者类型:1-用户(统计红娘发送的未读) 2-红娘(统计用户发送的未读)
+     */
+    public int countUnreadMessages(Long matchmakerId, Long userId, Integer readerType) {
+        Integer senderType = (readerType == 1) ? MatchmakerChatMessage.SENDER_TYPE_MATCHMAKER 
+                                                : MatchmakerChatMessage.SENDER_TYPE_USER;
+        return matchmakerChatMessageMapper.countUnreadMessages(matchmakerId, userId, senderType);
+    }
+}