【Java项目-轻聊】11-实现消息管理模块-获取历史消息

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

🎯 你正在阅读「Java项目-轻聊」系列文章 🎯

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

🔥 弹简特 个人主页

❄️ 个人专栏直通车:

靠热爱去书写自己,靠勇敢去书写生活!

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨


🌟 博主简介:


文章目录:


一、前言

我们轻聊项目最核心的部分就是我们本期所介绍的消息管理模块了,我们的消息管理中会介绍:客户端获取历史消息,客户端发送消息,接受消息,服务器使用WebSocket推送消息。

而由于内容稍微偏多些,我们会慢慢一篇一篇的介绍对应的内容,本期我们就先介绍客户端获取历史消息

二、准备

1、消息怎么传输

所谓的消息传输,它的本质是张三把消息通过服务器转发给李四,它有一个中转的过程

🤔:那为什么张三不直接发给李四,为什么非要通过服务器去发呢?非要通过服务器中转呢?

答:这件事情的万恶之源在于ipv4的地址不太够用,无法做到给每个设备都分配一个唯一的IP。它能表示的数据最多就是42亿9,000万所以要给全世界每个设备都分配一个ip地址是做不到的。

那么此时解决这个问题的办法有三个:

  • 第1个是动态ip的分配:就是上网的就分配,不上网就不分配
  • 第2个是NAT 机制:我们把ip分成内网ip和外网ip,一个外网ip可以代表若干个内网ip
  • 第3个是ipv6。

而在当前的一个背景下,我们使用的是第2种方法,使用nat机制,用一个比较稀缺的外网ip来代表一大波的内网ip。

所以此时很多个设备共用同一个外网ip,这样我们所消耗的ip数量就大大降低。

但是正是在这样的nat机制下,导致我们上述张三必须得通过服务器来中转发给李四,为啥张三不直接发给李四,因为张三根本就访问不了李四的设备。

张三李四都是普通的客户端,所以他们的设备大概率是没有外网ip的,只有内网ip。而如果没有外网ip,只有内网ip的话,则不能被直接访问到了,这种情况是由nat机制导致的,他就是这样。

除非张三和李四在同一个局域网里,否则他们两个无法直接进行通信。

它本质上就是我们的ipv4不够用,然后我们引入一个nat,而这个nat 却导致了:我们的内网和外网之间出现了一道鸿沟,即你内网可以直接访问外网,但是外网无法直接访问内网的设备。

所以你现在想要两个内网设备之间通信咋办?就得以一个大家都能够访问的,带有万网ip的设备作为桥梁,作为一个中转站。虽然张三不能直接访问的李四,但是张三可以访问服务器,李四不能直接访问张三,但是李四可以访问服务器。那么张三和服务器建立一个连接,李四和服务器也建立一个连接。那么服务器就可以同时和张三李四分别进行通信。此时张三有啥话要对李四说,张三就会通过服务器进行转发,转达给李四。

一方面:

之所以要通过服务器进行中转,就是因为ipv4不够用。然后我们又引入nat机制nat它本身就会导致这个问题:

使用服务器中转,最大原因就是nat背景下两个内网的设备无法直接进行通讯。[但是前提是不在同一个局域网内,在同一个局域网内的话就可以通信了,我们不用管。因为在同一个局域网内,连同一个路由器,就没必要那么麻烦。]

另一方面:

原因是通过服务器中转,是更容易在服务器这里记录历史消息,后续随时方便,我们来查询历史记录。

2、数据库设计

如果我们上述所说,我们两个人之间发消息,我们就得在服务器这里边使用数据库进行保存消息。那此时就得进行数据库设计,设计需要哪些表?

回顾一下,我们这里边的消息和我们前边的会话模块是相关的,在讲相关会话模块的时候,谈到了三个实体,用户实体,会话实体和消息实体。

我们的会话是一个中间的媒介,用户和会话是一个多对多的关系。

会话和消息是一个一对多的关系【一个会话里有多个消息,然后一个消息呢,只能从属于一个会话,是隶属关系】

然后咱们前面已经有了会话表message_session了。

所以呢,这里边我们的消息表只需要让他的每个记录带有会话表的ID,毕竟一对多的关系是在多的一方加入我们的隶属关系(外键加载多的一方),把隶属关系加入到多的一方中,代表我这个多隶属于哪个一的一方。

比如:

message_session表(1的一方)

主键 最后时间
1 '2000-01-01 00:00:00'

那么我们的消息表(多的一方)

message

主键 会话id
10 1
11 1

那么我们就可以设计这个消息表中有哪些字段

名称 描述
messageId 消息id
formId 就是消息是谁发的,那个用户是谁?
sessionid 然后呢消息发给哪个会话就是这个消息隶属于哪个会话
context 消息的正文存储消息的内容
posTime 消息的发送时间,比如我们的消息列表是按照消息的时间顺序来排序的

注意这里的关系,不是说消息从一个用户发给另一个用户,而是消息从一个会话发给另一个会话。

其实一个会话里可以有多个用户,它不只是两个用户。那你谁在群里发一个消息,大家在群里都能看到。

我们不需要有一个目标ID这种,我们只需要关注这个消息是谁发的就行了,然后会话里面所有人就都能看到。

三、获取某会话中最后一条历史消息

还记得我们之前在学习获取会话列表的时候,每一个会话的最后一条消息都是一样的,我们自己构造的数据

那么现在我们有了消息表之后,会话中的最后一条数据我们就可以实时查询出来了:

持久层:

java 复制代码
@Mapper
public interface MessageMapper {
    /**
     * 根据会话 ID 查询最后一条消息
     * @param sessionId 会话id
     * @return 返回最后一条消息
     */
    String getLastMessageBySessionId(Integer sessionId); 
}
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhongge.web_chatroom.dao.mapper.MessageMapper">
    <select id="getLastMessageBySessionId" resultType="java.lang.String">
        <!-- 原生SQL语句,MyBatis会解析后交给数据库执行 -->
        select content
        from message  -- 操作message聊天消息表
        where sessionId = #{sessionId}  -- 条件:匹配会话ID
        <!-- #{sessionId}:MyBatis占位符,预编译参数
            底层生成PreparedStatement,使用?占位,自动防SQL注入,推荐使用;
            参数名sessionId对应Mapper接口方法入参名称
        -->
        order by postTime desc  -- 根据消息发送时间postTime倒序,最新消息排在最前面
        limit 1  -- 只取第一条数据,也就是该会话最新的一条消息
    </select>
</mapper>

服务层代码

java 复制代码
    @Override // 重写接口抽象方法
    public List<MessageSession> getSessionList(int userId) {
        // 定义最终要返回给前端的会话列表
        List<MessageSession> messageSessionList = new ArrayList<>();
        // 边界校验:用户ID不合法,直接返回空列表
        if (userId <= 0) {
            System.out.println("用户ID不合法,无法查询会话列表");
            return messageSessionList;
        }

        // 1. 调用Mapper:查询当前用户关联的所有会话ID集合(按最后聊天时间倒序)
        List<Integer> sessionIdList = messageSessionMapper.getSessionIdByUserId(userId);
        /**
         * 使用LinkedHashMap:
         * 使用这个数据结构可以实现去重处理
         * 1. 实现按【好友ID】去重:同一好友只保留一条会话
         * 2. 保留插入顺序,维持数据库查询的时间排序
         * key:好友ID  value:对应会话实体
         */
        Map<Integer, MessageSession> sessionByFriendId = new LinkedHashMap<>();

        // 遍历每一个会话ID,逐个封装会话数据
        for (int sessionId : sessionIdList) {
            // 2. 根据会话ID + 当前用户ID,查询会话内的对方好友信息
            // 根据会话id 将我们会话表中除了用户自己的好友id给查询出来
            // 但是你放心,你查询出的结果一个会话只会有一个好友,因为你是单聊
            List<Friend> friends = messageSessionMapper.getFriendsBySessionId(sessionId, userId);
            // 容错:会话无有效聊天对象,跳过当前会话
            if (friends == null || friends.isEmpty()) {
                continue;
            }
            // 取第一个好友(你也只能取出第一个好友,因为你是单聊,那么你这个会话中就只有一个好友)
            int friendId = friends.get(0).getFriendId();

            // 组装会话实体对象
            MessageSession messageSession = new MessageSession();
            messageSession.setSessionId(sessionId); // 设置会话ID
            messageSession.setFriends(friends); // 绑定好友信息(昵称、头像、ID)

            // TODO 3. (已实现)查询该会话的最后一条消息内容(目前随机设置一个,到时候处理消息模块的时候就需要设置具体值)
            // messageSession.setLastMessage("你好,晚上我们一起去吃火锅吧~");
            // 3. 查询该会话的最后一条消息内容
            String lastMessage = messageMapper.getLastMessageBySessionId(sessionId);
            // 三目运算:无消息则赋值空字符串,避免前端null报错
            messageSession.setLastMessage(lastMessage == null ? "" : lastMessage);

            // 根据好友ID查找Map中是否已存在该好友的会话(这一步的作用就是用于去重的)
            MessageSession existing = sessionByFriendId.get(friendId);
            if (existing == null) {
                // 该好友首次出现,直接存入Map
                sessionByFriendId.put(friendId, messageSession);
            } else {
                // 如果这个好友已经存在在我们的列表中的话,那么我们就会使用最新的数据进行覆盖
                // 他的场景就是,你创建出了多个会话id的时候,我们怎么办的问题(多考虑特殊情况)
                // 该好友存在多条重复会话,调用方法筛选出最优会话进行覆盖
                sessionByFriendId.put(friendId, pickBetterSession(existing, messageSession));
            }
        }
        // 将去重、筛选完成后的会话集合转为List,作为最终结果
        // sessionByFriendId.values() 我们会有多个会话,那么每个会话中都会有一个好友
        // 所以此时我们sessionByFriendId集合中势必会有多个好友的信息,那么sessionByFriendId.values()就是将所有的key对应的会话信息返回(包括好友信息)
        messageSessionList.addAll(sessionByFriendId.values());
        return messageSessionList;
    }

测试

由图可知,我们出现了报错

解决: 注入对象

结果: 此时运行结果正确

四、获取会话历史消息

1、约定前后端交互接口

内容
URL GET /message?sessionId={sessionId}
是否需登录 否(前端在登录后调用)

成功响应 (JSON 数组,时间升序,最多 100 条)

json 复制代码
[
  {
    "messageId": 1,
    "fromId": 1,
    "fromName": "张三",
    "sessionId": 1,
    "content": "晚上吃什么",
    "avatarPath": "/avatars/xxx.jpg"
  }
]

注意:获取历史消息我们虽然说不需要先登录,但是其实他隐含的是先登录,为什么呢?

因为我们消息和会话之间的关系是:一个会话中,会有多个消息,一个消息只能隶属于一个会话,那么就可以根据会话id查询出这个会话中的所有消息,所以你的条件是有会话id,而这个会话id是在用户登录之后获取用户会话信息的时候获得的,此时就说明,要有会话id,就务必要先登录。

2、一张图理清楚业务逻辑

3、后端实现

3.1 持久层实现

Java和数据库的映射实体类

java 复制代码
import lombok.Data; // Lombok

@Data // 聊天消息 message 表实体,兼历史消息接口返回体
public class Message {
    private Integer messageId; // 消息主键
    private Integer fromId; // 发送者 userId
    private String fromName; // 发送者用户名(查询时 join user 表得到)
    private Integer sessionId; // 所属会话 ID
    private String content; // 消息正文
    private String avatarPath; // 发送者头像路径,渲染气泡旁头像
}

此处我们持久层MessageMapper接口上述已经创建好了,这里我们就写接口和sql映射文件

java 复制代码
    /**
     * 进入会话拉取历史(最多 100 条)
     * @param sessionId 会话id
     * @return 返回历史消息
     */
    List<Message> getHistoryMessageBySessionId(Integer sessionId);

sql映射文件:

xml 复制代码
<!--
    查询指定会话的历史聊天消息
    功能:根据会话ID,联查消息表+用户表,获取发送人昵称、头像,最多返回最新100条记录
    id:对应Mapper接口中 getHistoryMessageBySessionId 方法名
    resultType:返回数据封装到 Message 实体类 com.zhongge.web_chatroom.dao.dataobject.Message
-->
<select id="getHistoryMessageBySessionId" resultType="com.zhongge.web_chatroom.dao.dataobject.Message">
    <!--
        查询字段说明:
        message.messageId 消息主键ID
        message.fromId    发送者用户ID
        user.username as fromName 发送人用户名,别名fromName映射实体属性
        message.sessionId 所属聊天会话ID
        message.content   消息文本内容
        user.avatar_path as avatarPath 发送人头像路径,别名avatarPath映射实体属性
    -->
    select message.messageId, message.fromId, user.username as fromName,
           message.sessionId, message.content, user.avatar_path as avatarPath
    <!-- 关联两张表:消息表message、用户表user -->
    from message, user
    <!-- 关联条件:用户表userId = 消息发送者fromId,关联出该条消息发送人的信息 -->
    where user.userId = message.fromId
    <!-- 业务条件:只查询传入sessionId对应会话的消息 -->
    and message.sessionId = #{sessionId}
    <!-- #{sessionId} MyBatis预编译占位符,接收接口传入的会话ID参数,防SQL注入 -->
    <!-- 根据消息发送时间倒序,最新消息排在前面 -->
    order by postTime desc
    <!-- 限制最多查询100条,避免一次性加载海量消息造成页面/数据库压力 -->
    limit 100
</select>

3.2 服务层实现

接口:

java 复制代码
public interface MessageService {

    // 按会话 ID 获取历史消息(时间正序)
    List<Message> getHistoryMessages(Integer sessionId);
}

实现类:

java 复制代码
@Service // 注册为 Spring 管理的聊天消息业务服务 Bean
public class MessageServiceImpl implements MessageService { // 聊天消息业务服务实现类

    private final MessageMapper messageMapper; // 消息数据访问层

    // 构造注入消息 Mapper
    public MessageServiceImpl(MessageMapper messageMapper) {
        this.messageMapper = messageMapper; // 注入消息 Mapper
    }

    @Override // 实现获取历史消息
    public List<Message> getHistoryMessages(Integer sessionId) {
        if (sessionId == null) { // 会话 ID 为空
            return new ArrayList<>(); // 返回空列表
        }
        List<Message> messages = messageMapper.getHistoryMessageBySessionId(sessionId); // 按会话 ID 查询消息(数据库倒序)
        Collections.reverse(messages); // 反转为时间正序,便于前端按时间线展示
        return messages; // 返回历史消息列表
    }
}

3.3 控制层实现

java 复制代码
@RestController // 消息模块 REST 入口
public class MessageController { // 提供某会话下的历史聊天记录

    @Resource // 注入消息业务服务
    private MessageService messageService; // 从数据库按 sessionId 查询消息列表

    @GetMapping("/message") // 根据会话 ID 获取历史消息(进入聊天页时加载)
    public Object getMessage(Integer sessionId) { // sessionId 为两人会话在库中的主键
        return messageService.getHistoryMessages(sessionId); // 按时间排序返回消息 DTO 列表
    }
}

4、前端实现

现在对于前端来说:什么时候去获取历史消息,以及怎么渲染的问题,对于渲染所需要的样式我们之前就已经借助AI完成对应UI的美化。

3.1 参数会话id在哪里

我们之前创建会话的时候,创建成功会返回有会话id,那么我们之前是将他存在标签属性中的

还有我们之前获取会话列表信息返回有会话id,我们将他存在了标签属性中

你要获取哪一个会话中的所有聊天消息,那么你就得提供会话id,而我们的会话id恰恰在之前我们就保存下来了,所以此处我们便可以直接获取会话了。

3.2 哪些地方需要获取历史会话信息

  1. 人员列表点击的时候,跳到会话列表,并自动打开消息框,加载聊天信息

  2. 会话列表中,点击对应的会话的时候,就会打开消息框,加载聊天信息

3.3 前端获取历史信息

js 复制代码
// GET 历史消息并渲染右侧聊天气泡
/**
 * 根据会话ID加载该会话全部历史聊天记录,渲染到右侧聊天窗口
 * @param {number|string} sessionId 聊天会话唯一标识
 */
function getHistoryMessage(sessionId) {
    // 1. 获取存放聊天记录的容器DOM(右侧消息展示区域)
    let messageShowDiv = document.querySelector('.message-show');
    // 清空容器原有消息,切换会话时清除上一轮聊天记录
    messageShowDiv.innerHTML = '';

    // 2. 获取右侧顶部对方昵称+头像标题栏DOM
    let titleDiv = document.querySelector('.right .title');
    // 先清空标题栏原有内容
    titleDiv.innerHTML = '';

    // 3. 获取左侧会话列表中当前被选中的会话li标签(带selected选中样式)
    let selectedLi = document.querySelector('#session-list .selected');
    // 判断是否存在选中会话(防止未选中会话时报错)
    if (selectedLi) {
        // 从选中li的自定义属性读取好友ID
        let friendId = selectedLi.getAttribute('data-friend-id');
        // 从选中li的自定义属性读取好友头像文件路径
        let avatarPath = selectedLi.getAttribute('data-avatar-path');
        // 调用工具方法,获取当前会话展示名称(单聊=好友名)
        let name = getSessionDisplayName(selectedLi);

        // 给标题栏追加样式类,用于显示头像布局
        titleDiv.className = 'title title-with-avatar';
        // 拼接标题栏HTML:头像标签 + 转义后的好友昵称
        // avatarImgHtml:生成头像图片DOM片段
        // escapeHtml:对昵称做HTML转义,防止XSS注入、特殊符号渲染错乱
        titleDiv.innerHTML = avatarImgHtml(friendId, 'avatar-md', avatarPath)
            + '<span class="title-name">' + escapeHtml(name) + '</span>';
    }

    // 4. 发送AJAX GET请求,后端查询该会话所有历史消息
    $.ajax({
        // 请求方式 GET
        method: 'get',
        // 请求地址,拼接当前会话ID作为请求参数
        url: '/message?sessionId=' + sessionId,
        // 请求成功回调函数,body为后端返回的消息数组
        success: function(body) {
            // 遍历后端返回的每一条聊天消息对象
            for (let message of body) {
                // 调用封装方法,单条消息生成聊天气泡并追加到消息容器
                addMessage(messageShowDiv, message);
            }
            // 渲染完所有消息后,自动滚动到容器最底部,展示最新一条消息
            scrollBottom(messageShowDiv);
        }
    });
}

/**
 * 在消息展示容器中新增一条聊天气泡,自动区分自己发送(右侧气泡)、他人发送(左侧气泡)
 * @param {HTMLElement} messageShowDiv 存放所有聊天消息的父容器DOM
 * @param {Object} message 单条消息数据对象,包含 fromId、fromName、content、avatarPath 等字段
 */
function addMessage(messageShowDiv, message) {
    // 创建最外层消息块div,单条消息整体容器
    let messageDiv = document.createElement('div');
    // 获取当前登录用户的用户名(工具函数,全局保存的登录账号)
    let selfUser = getCurrentUsername();
    // 判断这条消息是不是当前登录用户自己发的:发送者用户名 == 登录用户名
    let isSelf = selfUser === message.fromName;

    // 动态设置样式类
    // message-right:本人消息靠右展示
    // message-left:别人消息靠左展示
    messageDiv.className = isSelf ? 'message message-right' : 'message message-left';

    // 拼接气泡内部HTML:包裹消息文本,使用escapeHtml转义防止XSS攻击
    let bubbleHtml = '<div class="bubble"><p class="bubble-text">'
        + escapeHtml(message.content) + '</p></div>';
    // 调用工具函数生成头像img标签,参数:发送人ID、小尺寸头像样式、头像图片地址
    let avatarPart = avatarImgHtml(message.fromId, 'avatar-sm', message.avatarPath);

    // 布局区分:
    // 自己发的消息:气泡在前,头像在后(靠右布局,文字左头像右)
    if (isSelf) {
        messageDiv.innerHTML = bubbleHtml + avatarPart;
    } else {
        // 他人消息:头像在前,气泡在后(靠左布局,头像左文字右)
        messageDiv.innerHTML = avatarPart + bubbleHtml;
    }

    // 将组装完成的单条消息DOM,追加到消息列表容器中渲染到页面
    messageShowDiv.appendChild(messageDiv);
}

/**
 * 让指定容器滚动条自动滑到最底部,聊天打开新消息时自动定位最新消息
 * @param {HTMLElement} elem 需要滚动的消息容器DOM对象
 */
function scrollBottom(elem) {
    // offsetHeight:元素可视区域高度(不含滚动隐藏部分,固定显示区域大小)
    let clientHeight = elem.offsetHeight;
    // scrollHeight:容器内全部内容的总高度,包含超出可视区域、需要滚动才能看到的内容
    let scrollHeight = elem.scrollHeight;
    // 滚动到垂直最底部:总内容高度 - 可视高度 = 最大可向下滚动距离
    elem.scrollTo(0, scrollHeight - clientHeight);
}

/**
 * 获取当前登录用户的用户名,用来区分消息是自己发的还是别人发的,控制气泡左右展示
 * @returns {string} 当前登录用户名;找不到用户名称元素时返回空字符串
 */
function getCurrentUsername() {
    // 选择器定位页面左侧个人信息区域的用户名DOM元素
    // 层级:主容器.main → 左侧侧边栏.left → 用户模块.user → 用户名称标签.user-name
    let nameEl = document.querySelector('.main .left .user .user-name');
    // 三元判断:如果元素存在,取出内部纯文本作为用户名;不存在则返回空字符串
    // 做判空防止nameEl为null时调用textContent抛出JS空指针报错
    return nameEl ? nameEl.textContent : '';
}

逻辑:

text 复制代码
选中会话
  │
  ├─ 清空消息区 + 标题栏
  ├─ 若有选中项 → 渲染对方头像/昵称
  ├─ 请求 GET /message?sessionId
  │     │
  │     └─ 成功 → 逐条 addMessage
  │               ├─ 取当前用户名
  │               ├─ 判断左/右气泡
  │               └─ 追加到页面
  └─ scrollBottom → 滚到最新消息

那么这个历史消息由谁来调用呢?

我们说了有两个:

  • 点击会话列表-->调用历史信息
  • 点击人员列表-->自动选中会话列表-->调用历史信息

3.4 点击会话列表调用历史信息

找到我们之前的clickSession方法变为如下:

js 复制代码
/**
 * 点击左侧会话列表项的事件处理函数
 * 功能:切换选中状态、展示聊天面板、清空未读消息、加载历史聊天记录
 * @param {HTMLElement} currentLi 当前点击的会话li元素
 */
function clickSession(currentLi) {
    // TODO:后续先处理你点击的会话是否是好友请求
    // 这里需要判断当前点击的会话是否是好友请求类型
    // 如果是好友请求,需要特殊处理显示不同的界面和逻辑
    
    
    // TODO:后续展示右侧聊天面板(已实现)
    // 需要根据点击的会话展示对应的聊天面板
    // 可能包括聊天窗口、消息输入区域等功能组件
    showChatPanel();


    //筛选出所有普通会话项(排除好友请求条目)
    let allLis = document.querySelectorAll('#session-list>li:not(.friend-request-item)');
    // 统一处理会话选中样式:取消其他项选中、给当前项添加选中样式
    activeSession(allLis, currentLi);
    // 调用activeSession函数处理会话的选中状态
    // 参数:所有普通会话项列表和当前点击的会话项
    // 实现功能:取消其他会话的选中状态,同时给当前点击的会话添加选中样式
    
    
    // TODO:获取当前会话唯一ID,后续用去请求会话中的历史消息(已实现)
    let sessionId = currentLi.getAttribute("message-session-id");
    // 需要从currentLi元素中提取会话的唯一标识符
    // 这个ID将用于后续获取该会话的历史消息记录


    // 用户主动点击会话-->未读清零-->角标消失
    // TODO:后续实现点击出现角标的样式
    // 需要实现点击会话时显示未读消息角标的功能
    // 可能包括角标的显示/隐藏逻辑、位置计算等


    // TODO:后续实现根据会话ID请求并加载历史聊天消息(已实现)
    // 需要实现通过会话ID向后端API发送请求
    // 获取并加载该会话的历史聊天消息到聊天面板中
    // 可能包括消息的分页加载、滚动定位等功能
    getHistoryMessage(sessionId);
}

3.5 点击人员列表到会话列表调用历史信息

找到我们之前的``代码完善如下:

java 复制代码
/**
 * 好友列表中点击好友触发事件
 * 逻辑:打开已有会话 / 新建会话,会话自动置顶并切换到会话标签页
 * @param {Object} friend 好友对象,包含 friendId、friendName、avatarPath 等信息
 */
// 好友 Tab 点击好友:打开已有会话或创建新会话
function clickFriend(friend) {
    //TODO 后续在消息模块中:先展示消息的聊天面板(已实现)
    showChatPanel();

    // 先根据好友ID、好友名称,在会话列表中查找对应的会话li元素
    // 优先按ID查找,找不到再按昵称匹配
    let sessionLi = findSessionByFriendId(friend.friendId) || findSessionByName(friend.friendName);
    // 获取整个会话列表容器
    let sessionListUL = document.querySelector('#session-list');

    // 分支1:该好友已有会话记录
    if (sessionLi) {
        // 将当前会话项移动到普通会话区域最顶部
        // getFirstChatSessionLi:获取第一个普通会话节点(跳过顶部好友请求项)
        sessionListUL.insertBefore(sessionLi, getFirstChatSessionLi(sessionListUL));
        // 主动触发点击事件,执行选中会话、清空未读、加载历史消息逻辑
        sessionLi.click();
    }
    // 分支2:暂无对应会话,需要新建会话
    else {
        // 创建空的会话li节点,作为临时占位项
        sessionLi = document.createElement('li');
        // 绑定好友ID,用于后续识别
        sessionLi.setAttribute('data-friend-id', friend.friendId);
        // 存在头像路径则绑定头像属性
        if (friend.avatarPath) {
            sessionLi.setAttribute('data-avatar-path', friend.avatarPath);
        }
        // 拼接会话项HTML,消息预览传空字符串(新建会话暂无消息)
        sessionLi.innerHTML = buildSessionLiHtml(friend.friendId, friend.friendName, '', friend.avatarPath);
        // 将新建的占位会话插入到普通会话最顶部
        sessionListUL.insertBefore(sessionLi, getFirstChatSessionLi(sessionListUL));

        // 新会话绑定点击事件
        sessionLi.onclick = function() {
            clickSession(sessionLi);
        };
        // TODO 后续实现 为先触发点击,打开聊天界面,提升交互体验(已实现)
        sessionLi.click();
        
        // 调用方法向后端请求创建会话,成功后回填会话ID
        createSession(friend.friendId, sessionLi);
    }

    // 自动切换页面标签,从【好友】Tab 切回【会话】Tab
    document.querySelector('.tab .tab-session').click();
}

5、测试

登录之后,点击会话列表,显示消息框,并加载历史消息渲染

点击好友列表-》选中好友-》点击好友李四-》跳到会话列表(置顶)-》自动选择和李四的会话-》打开消息聊天框并加载历史消息渲染


本期搞定【获取历史消息】🎉!🚀。

下期开始介绍消息推送用到的协议:WebSocket协议🖥️!

干货持续更新,记得点赞👍关注🌟收藏⭐,追更不迷路~