山东大学软件学院2026项目实训个人博客(七)
项目名称基于AI大模型的智能考研社区撰写日期2026年5月31日本周在上一周已经实现部分聊天功能接口的基础上进一步实现了其余的聊天功能优化实现逻辑整理完善已有代码并完成本地测试。一、聊天功能整体架构前端↓ HTTP (JWT Token 认证)MessageController ← 也调用 UserService搜索用户↓IMessageService (接口)↓MessageServiceImpl (核心实现)MySQL: message / message_list / friend 三张表Redis: 聊天记录缓存WebSocket (MessageServer): 实时推送依赖: UserMapper, TopicMapper, FriendMapper, MessageListService所有接口经过 JwtTokenUserInterceptor 拦截从请求头 Authorization 中解析 JWT将 userId 存入 BaseContext ThreadLocal后续所有方法通过 BaseContext.getCurrentId() 获取当前用户。二、各功能实现逻辑1. 发送消息 POST /message/send前端 → Controller.send() → Service.send()Controller 层 从 BaseContext.getCurrentId() 取当前登录用户 ID 强制覆盖 senderId 调用 messageService.send(messageDTO)Service 层核心流程 1. 好友校验查 friend 表:(userIdOneA AND userIdTwoB AND status1) OR (userIdOneB AND userIdTwoA AND status1)不存在 → 抛 还不是好友无法发送消息2. 构建 Message 对象 校验通过content / senderId / receiverId / typeisRetract0 isRead0isDeletedSender0 isDeletedReceiver0senderTime当前时间3. 写数据库this.save(message) → INSERT INTO message4. 清除 Redis 缓存DEL message:chat:A:BDEL message:chat:B:A5. 更新会话列表 message_list要是错题消息会话显示 错题 而非题目IDmessageListService.updateMessageList(senderId, receiverId, content, time)→ 查是否存在该会话有则 notReadCount1无则新建一条6. WebSocket 双路推送推送1 (type1): 新消息 VO → 接收方- 含 senderName, senderAvatar- 错题消息含 topicId, topicTitle 卡片信息推送2 (type2): 更新后的会话列表 VO → 接收方2. 获取聊天记录 GET /message/listMessage前端 → Controller.getMessages() → Service.getMessages(userId, otherUserId, pageNum)核心流程 1. 尝试 Redis 缓存KEY message:chat: userId : otherUserId命中 → markMessagesAsRead() → 直接返回2. 查 MySQL 若缓存未命中SELECT * FROM message WHERE ((senderIdA AND receiverIdB) OR (senderIdB AND receiverIdA)) AND isDeletedSender0 ORDER BY senderTime DESC LIMIT 20 OFFSET (pageNum-1)*203. 组装 MessageVO每条消息由如下信息组成查 sender 的 name 和 avatar错题消息: 解析 content 为 topicId,查 topic 表获取 titleisRead 状态 .4. 标记已读 缓存 返回markMessagesAsRead(userId, otherUserId)cacheMessages(key, list, 30分钟)return messageVOList其中已读标记逻辑 markMessagesAsRead UPDATE message SET isRead1 WHERE senderId对方 AND receiverId我 AND isRead0 → 清除双方缓存 → WebSocket type4 通知对方 我已读3. 撤回消息 POST /message/retract/Controller → retractMessage(request, userId)流程1. 查消息: this.getById(messageId)2. 校验: 消息存在发送者是自己3. 时间校验: senderTime now - 3分钟 → 抛 超过三分钟不能撤回4. UPDATE message SET isRetract15. 清除双方缓存6. 更新会话列表: notReadCount - 17. WebSocket type3 推送撤回通知 → 只推送消息ID4. 获取会话列表 POST /message/listMessageListController → getMessageLists(queryDTO)流程1. 查 message_list 表:WHERE (senderId我 OR receiverId我) AND isAppear1 ORDER BY recentTime DESC可选过滤: recentTime 上限、对方用户名模糊匹配2. 对每条会话记录:- 确定对方是谁 (senderId我 ? receiverId : senderId)- 查对方 user 的 name 和 avatar- 组装 MessageListVO(userId, userName, userAvatar,unreadCount, recentContent, recentTime)- 支持按 name 过滤在循环中二次过滤5. 好友请求 POST /message/sendFriendRequestController → sendFriendRequest(friendMakeDTO)流程1. receiverId friendMakeDTO.getReceiverId()校验: 不能给自己发请求2. 双向查 friend 表防重复:(userIdOne我 AND userIdTwo对方)OR (userIdOne对方 AND userIdTwo我)已存在记录的处理:├─ status1(已是好友) → 抛 已经是好友├─ status0(待处理) → 抛 已发送过请求└─ status2/3/4(被拒绝/拉黑) → 覆盖重发:status→0, incidentalMessage→新消息, sendTime→now, becomeTime→null不存在 → INSERT 新记录:userIdOne我, userIdTwo对方, status0(FRIEND_WAIT)3. WebSocket type5 通知接收方 收到新的好友请求6. 接受/拒绝好友 POST /message/feedbackFriendRequestController → feedbackFriendRequest(friendMakeDTO)流程1. requesterId friendMakeDTO.getReceiverId() ← 请求方的ID当前用户 BaseContext.getCurrentId() ← 我接受/拒绝方2. 查 friend 表:WHERE userIdOnerequesterIdAND userIdTwo我AND status0(FRIEND_WAIT)不存在 → 抛 未找到待处理的好友请求3. acceptFlag1 → status→1(接受), becomeTime→nowacceptFlag≠1 → status→2(拒绝)4. UPDATE friend5. WebSocket type5 通知请求方对方已接受你的好友请求 / 对方已拒绝你的好友请求7. 查看好友请求列表 POST /message/getFriendRequestListController → getFriendRequestList(friendQueryDTO)流程1. 查 friend 表:WHERE userIdTwo我 (别人发给我的申请) ORDER BY sendTime DESC可选: AND status指定值2. 对每条记录:查 userIdOne(请求方) 的 name 和 avatar组装 FriendRequestVO(id请求方ID, name, avatar,status, incidentalMessage, sendTime)8. 解除好友 POST /message/deleteFriendController → deleteFriend(friendMakeDTO)流程1. 双向查 friend 表:(userIdOne我 AND userIdTwo对方 AND status1)OR (userIdOne对方 AND userIdTwo我 AND status1)不存在 → 抛 好友关系不存在2. 物理删除: friendMapper.deleteById(id)9. 搜索用户 POST /message/searchDefaultUser 和 POST /message/searchFriend这两个在 UserServiceImpl 中实现被 MessageController 调用。1searchDefaultUser 搜所有非管理员用户WHERE role≠1(管理员) AND deleted0 AND status1(正常) AND id≠自己 可选: name LIKE %关键字%→ 返回 UserVO list, isFriend02searchFriend 搜自己的好友第1步: SELECT * FROM friend WHERE (userIdOne我 OR userIdTwo我) AND status1 第2步: 提取好友ID集合 第3步: SELECT * FROM user WHERE id IN (好友ID集合) AND deleted0 AND status1 可选: name LIKE %关键字%→ 返回 UserVO list, isFriend1三、WebSocket 推送体系所有实时通知通过 MessageServer.sendToUser(userId, message) 完成使用 type 字段区分type含义携带data1新消息通知MessageVO 完整消息对象2会话列表更新MessageListVO 会话对象3消息撤回消息 ID (Long)4已读状态更新对方用户 ID (Long)5好友请求/反馈提示文本 (String)MessageServer 内部维护一个 ConcurrentHashMapLong, Session OnOpen 时注册 OnClose 时移除。四、Redis 缓存策略操作缓存行为发送消息清除双方缓存 message:chat:A:B 和 message:chat:B:A获取聊天记录优先读缓存命中直接返回未命中查 DB 后写入 TTL 30 分钟撤回消息清除双方缓存标记已读清除双方缓存