在线聊天(单聊)案例
# 在线聊天(单聊)案例
# 1. 数据库表结构
# 聊天组表 (chat_group
)
CREATE TABLE `chat_group` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID - 主键,自动递增',
`chat_user_id` int(11) NULL DEFAULT NULL COMMENT '聊天用户ID - 表示与谁进行聊天的用户ID',
`user_id` int(11) NULL DEFAULT NULL COMMENT '当前用户ID - 当前登录用户的ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET=utf8mb4 COMMENT '聊天组 - 存储用户与用户之间的聊天关系';
2
3
4
5
6
详细说明:
表名:
chat_group
表名直观表达了存储聊天组的意图,即存储用户之间的聊天关系。字段详细说明:
id
: 这是主键,用于唯一标识每个聊天组。设置为自增是因为每个聊天组需要一个唯一的标识符,且由数据库自动生成。chat_user_id
: 表示与当前用户进行聊天的另一位用户的ID。这个字段与user_id
一起确定了聊天的两方。这在后续的查询中会非常有用,特别是在获取两位用户之间的聊天记录时。user_id
: 当前登录用户的ID。这个字段在整个聊天流程中起着关键作用,因为它表示了当前登录的用户。无论是创建聊天组还是获取聊天记录,都会用到这个字段。
设计意图:
- 这个表的设计目的是存储用户与用户之间的聊天关系。每一条记录代表一个用户与另一用户之间的聊天组。
- 使用
chat_user_id
和user_id
组合能够确定唯一的聊天组,并且可以根据这两个字段快速查找到相关聊天信息。
# 聊天信息表 (chat_info
)
CREATE TABLE `chat_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID - 主键,自动递增',
`chat_user_id` int(11) NULL DEFAULT NULL COMMENT '聊天用户ID - 接收消息的用户ID',
`user_id` int(11) NULL DEFAULT NULL COMMENT '当前用户ID - 发送消息的用户ID',
`text` varchar(1000) NULL DEFAULT NULL COMMENT '聊天内容 - 消息的文本内容',
`isread` varchar(255) NULL DEFAULT '否' COMMENT '是否已读 - 表示消息是否已被接收者读取,默认为未读',
`time` varchar(255) NULL DEFAULT NULL COMMENT '时间 - 消息发送的时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET=utf8mb4 COMMENT '聊天信息 - 存储每条聊天记录';
2
3
4
5
6
7
8
9
详细说明:
表名:
chat_info
表名直接说明了该表存储的是具体的聊天信息。字段详细说明:
id
: 这是主键,用于唯一标识每条聊天记录。设置为自增是因为每条聊天记录都需要一个唯一的标识符,且由数据库自动生成。chat_user_id
: 表示接收消息的用户ID。在聊天过程中,我们需要知道消息的接收者是谁,以便在消息发送后能够将其传递给正确的用户。user_id
: 表示发送消息的用户ID。这个字段用来标识是谁发送了这条消息,在聊天记录中用于区分对话双方。text
: 聊天内容字段,用于存储消息的实际文本内容。最大长度为1000字符,足以应对大部分的聊天场景。isread
: 用于标记消息是否已被接收者读取。默认值设置为“否”,因为一条消息刚发送时通常是未读的。time
: 记录消息发送的时间。时间戳对于聊天记录的排序、显示等操作非常重要。
设计意图:
- 这个表的设计目的是存储具体的聊天信息。每一条记录代表一条消息。
- 通过
chat_user_id
和user_id
,我们可以确定这条消息的发送者和接收者,并根据isread
字段判断消息是否已读。
# 2. 实体类
# 用户表实体类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Builder;
import lombok.Data;
/**
* 用户表
* @TableName user
*/
@TableName(value ="user")
@Data
@Builder
public class User implements Serializable {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码,已加密
*/
private String password;
/**
* 邮箱
*/
private String email;
/**
* 电话
*/
private String phone;
/**
* 账户状态:1-启用,0-禁用
*/
private Integer status;
/**
* 头像URL
*/
private String avatar;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# ChatGroup 实体类
package com.example.chat.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("chat_group") // 指定与数据库表 `chat_group` 对应
public class ChatGroup {
@TableId(type = IdType.AUTO) // 指定主键自增
private Integer id; // 主键ID
private Integer chatUserId; // 聊天用户ID
private Integer userId; // 当前用户ID
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
详细说明:
类名:
ChatGroup
类名清晰地表达了这个类的作用,即对应数据库中的chat_group
表,用于存储和操作聊天组的数据。注解详细说明:
@TableName("chat_group")
: 这个注解将该实体类与数据库中的chat_group
表关联起来。通过这个注解,MyBatis Plus 可以自动将ChatGroup
实体类映射到chat_group
表中进行增删改查操作。@TableId(type = IdType.AUTO)
: 这个注解用于标识id
字段为表的主键,并且设置其为自增类型。MyBatis Plus 会根据这个注解自动处理主键的生成和插入。
字段详细说明:
id
: 主键字段,用于唯一标识每个聊天组。由于设置了@TableId(type = IdType.AUTO)
注解,系统会自动为其赋值。chatUserId
: 用于存储与当前用户进行聊天的另一位用户的ID。userId
: 用于存储当前用户的ID。在业务逻辑中,这个字段非常重要,因为它决定了这个聊天组的所有权是谁。
设计意图:
- 该实体类的设计目的是为了方便与
chat_group
表进行数据交互。通过这个类,我们可以在业务逻辑中更直观地操作聊天组数据,例如创建新聊天组、查找现有聊天组等。
- 该实体类的设计目的是为了方便与
# ChatInfo 实体类
package com.example.chat.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("chat_info") // 指定与数据库表 `chat_info` 对应
public class ChatInfo {
@TableId(type = IdType.AUTO) // 指定主键自增
private Integer id; // 主键ID
private Integer chatUserId; // 聊天用户ID,接收消息的用户ID
private Integer userId; // 当前用户ID,发送消息的用户ID
private String text; // 聊天内容
private String isread; // 是否已读,默认“否”
private String time; // 消息发送的时间
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
详细说明:
类名:
ChatInfo
类名直接表明了这个类与聊天信息相关,具体来说是与chat_info
表进行映射和操作。注解详细说明:
@TableName("chat_info")
: 这个注解将该实体类与数据库中的chat_info
表关联起来。这样,当我们在业务逻辑中操作ChatInfo
对象时,MyBatis Plus 会自动映射到chat_info
表中的相应记录。@TableId(type = IdType.AUTO)
: 这个注解指定id
字段为表的主键,并设置为自增类型。MyBatis Plus 会在插入记录时自动生成并设置这个字段的值。
字段详细说明:
id
: 主键字段,用于唯一标识每条聊天记录。这个字段是必不可少的,因为它确保了每条聊天记录的唯一性。chatUserId
: 存储接收消息的用户ID。在聊天系统中,知道消息的接收者是谁是至关重要的,这个字段会在消息的发送和接收逻辑中被频繁使用。userId
: 存储发送消息的用户ID。与chatUserId
一起,这个字段帮助我们明确聊天的双方身份。text
: 用于存储聊天内容。这个字段的长度为1000个字符,能够容纳大部分普通聊天消息。这个字段将在前端聊天窗口中直接展示。isread
: 用于标识消息是否已读。默认值为“否”,表示新消息刚发送时,接收者尚未读取。这在用户体验中是很重要的,可以用于未读消息提醒。time
: 用于存储消息发送的时间。在聊天记录中,时间戳帮助用户理解对话的时间顺序,且可以用于排序显示。
设计意图:
- 该实体类的设计目的是为了映射和操作
chat_info
表中的聊天数据。通过这个类,开发者可以轻松地进行聊天记录的存储、查询、更新等操作。
- 该实体类的设计目的是为了映射和操作
# UserUnreadMessageDTO 实体类
@Data
public class UserUnreadMessageDTO {
private Long id; // 用户ID
private String username; // 用户名
private String avatar; // 用户头像
private Long unreadCount; // 未读消息数量
}
2
3
4
5
6
7
详细说明:
类名:
UserUnreadMessageDTO
直观地说明了这个类用于传输用户未读消息的数据。这是一个数据传输对象(DTO),专门用于封装从数据库中查询出来的用户信息和未读消息数量。字段详细说明:
id
: 存储用户的唯一标识符。每个用户都有一个唯一的ID,通过这个ID,我们可以将用户与其聊天记录、未读消息等关联起来。username
: 存储用户的用户名。在前端界面上,我们通常会展示用户名,这个字段直接对应用户在系统中的标识名称。avatar
: 存储用户的头像URL。头像在用户界面中非常重要,用于让用户之间的交流更加直观和人性化。unreadCount
: 存储当前用户从每个其他用户收到的未读消息数量。这个字段直接反映了用户界面上显示的未读消息提醒。
设计意图:
- 这个类的设计是为了在服务层与前端之间传递用户和消息数据。通过这个DTO,我们可以一次性获取并传输用户的基本信息和他们的未读消息数量,减少多次查询和数据处理的开销。
# 3. Mapper 接口
# ChatGroupMapper 接口
package com.example.chat.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.chat.entity.ChatGroup;
import org.apache.ibatis.annotations.Mapper;
@Mapper // 标识为 MyBatis Mapper 接口
public interface ChatGroupMapper extends BaseMapper<ChatGroup> {
}
2
3
4
5
6
7
8
9
详细说明:
接口名:
ChatGroupMapper
显式说明了这个接口用于操作ChatGroup
实体类,即与chat_group
表进行交互。注解详细说明:
@Mapper
: 这个注解将ChatGroupMapper
接口标识为 MyBatis 的 Mapper 接口。MyBatis 会自动扫描并生成该接口的实现类,用于与数据库进行交互。
继承关系说明:
BaseMapper<ChatGroup>
: 这个接口继承了 MyBatis Plus 提供的BaseMapper
,并指定了泛型为ChatGroup
。通过继承BaseMapper
,这个接口自动具备了对chat_group
表的常用操作(如增删改查)的能力,而不需要手动编写这些方法。
设计意图:
- 这个接口的设计目的是为了简化与数据库
chat_group
表的交互。在实际开发中,我们可以通过调用ChatGroupMapper
的方法来轻松地操作chat_group
表,而不需要编写重复的SQL语句。
- 这个接口的设计目的是为了简化与数据库
# ChatInfoMapper 接口
import org.apache.ibatis.annotations.Param;
public interface ChatInfoMapper extends BaseMapper<ChatInfo> {
/**
* 获取用户的未读消息信息列表
* @param currentUserId 当前用户ID
* @return 用户未读消息信息的 DTO 列表
*/
List<UserUnreadMessageDTO> getUserUnreadMessageInfo(@Param("currentUserId") String currentUserId);
}
2
3
4
5
6
7
8
9
10
11
详细说明:
接口名:
ChatInfoMapper
明确指出了这个接口用于操作ChatInfo
实体类,即与chat_info
表进行交互。继承关系说明:
BaseMapper<ChatInfo>
: 继承了 MyBatis Plus 提供的BaseMapper
,并指定了泛型为ChatInfo
。通过继承BaseMapper
,我们可以对chat_info
表进行常用的增删改查操作。
自定义方法说明:
getUserUnreadMessageInfo
: 这个方法用于获取指定用户的未读消息信息列表,返回一个包含用户信息和未读消息数量的 DTO 列表。
注解详细说明:
@Param("currentUserId")
: 用于将方法参数绑定到 SQL 查询中的变量#{currentUserId}
。这样可以确保在查询中正确传递用户ID。
设计意图:
- 这个接口的设计目的是为了专门处理与
chat_info
表相关的复杂查询。虽然BaseMapper
提供了基础的增删改查功能,但ChatInfoMapper
可以通过自定义方法扩展这些功能,例如查询未读消息。
- 这个接口的设计目的是为了专门处理与
# 4. Mapper XML 配置
# ChatInfoMapper.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.example.chat.mapper.ChatInfoMapper">
<resultMap id="BaseResultMap" type="com.example.chat.entity.ChatInfo">
<id property="id" column="id" jdbcType="INTEGER"/>
<result property="chatUserId" column="chat_user_id" jdbcType="INTEGER"/>
<result property="userId" column="user_id" jdbcType="INTEGER"/>
<result property="text" column="text" jdbcType="VARCHAR"/>
<result property="isread" column="isread" jdbcType="VARCHAR"/>
<result property="time" column="time" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
id, chat_user_id, user_id, text, isread, time
</sql>
<select id="getUserUnreadMessageInfo"
resultType="com.example.chat.dto.UserUnreadMessageDTO">
SELECT
u.id,
u.username,
u.avatar,
IFNULL(COUNT(c.id), 0) AS unreadCount
FROM
user u
LEFT JOIN
chat_info c
ON
u.id = c.user_id
AND
c.isread = '否'
AND
c.chat_user_id = #{currentUserId}
GROUP BY
u.id, u.username, u.avatar;
</select>
</mapper>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
详细说明:
文件用途: 这个 XML 文件定义了
ChatInfoMapper
中自定义查询的具体 SQL 语句。通过这个文件,MyBatis 能够将getUserUnreadMessageInfo
方法与 SQL 查询绑定起来。元素详细说明:
<mapper namespace="com.example.chat.mapper.ChatInfoMapper">
: 定义该 XML 文件对应的 Mapper 接口为ChatInfoMapper
。<resultMap>
: 这个元素用于定义结果集与实体类ChatInfo
的映射关系。它将数据库中的列名与实体类的属性名一一对应。<sql>
: 定义一个可复用的 SQL 片段,Base_Column_List
包含了chat_info
表的所有常用列。<select>
: 定义了一个查询语句,用于实现getUserUnreadMessageInfo
方法。这个查询通过LEFT JOIN
将user
表与chat_info
表关联,查询出每个用户的未读消息数量。
设计意图:
- 使用
<resultMap>
是为了确保查询结果能够正确映射到ChatInfo
实体类中,从而在业务逻辑中能更方便地操作数据。 - 使用
<sql>
定义列名列表是为了简化重复的 SQL 书写,这样当表结构发生变化时,只需要修改一次就可以了。 getUserUnreadMessageInfo
查询的设计是为了获取用户的基本信息和未读消息数量,并返回一个 DTO 列表,这对于实现未读消息提醒功能非常重要。
- 使用
# 5. Service 接口和实现
# ChatInfoService 接口
import java.util.List;
public interface ChatInfoService extends IService<ChatInfo> {
// 创建或获取聊天组
ChatGroup getOrCreateChatGroup(Integer userId, Integer chatUserId);
// 保存聊天信息
ChatInfo saveChatInfo(ChatInfo chatInfo);
// 获取聊天历史
List<ChatInfo> getChatHistory(Integer userId, Integer chatUserId);
// 将消息标记为已读
void markAsRead(Integer userId, Integer chatUserId);
// 获取用户的未读消息数
Long getUnreadCount(Integer userId);
// 获取用户信息和用户的未读消息数
List<UserUnreadMessageDTO> getUserUnreadMessageInfo(String currentUserId);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
详细说明:
接口名:
ChatInfoService
是服务层接口,定义了与chat_info
表相关的核心业务逻辑。通过这个接口,我们可以将业务逻辑与具体的持久层操作分离。方法详细说明:
getOrCreateChatGroup
: 这个方法用于创建或获取一个聊天组。传入的两个参数是当前用户ID和聊天用户ID。这是聊天开始前的必要步骤,确保两个用户之间有一个独立的聊天组。saveChatInfo
: 这个方法用于保存聊天信息。通过这个方法,我们可以将每条消息存储到chat_info
表中。getChatHistory
: 这个方法用于获取两位用户之间的聊天历史记录。通过传入用户ID和聊天用户ID,可以查询到两人之间的所有消息。markAsRead
: 这个方法用于将消息标记为已读。未读消息通常需要提醒用户,但一旦用户查看了消息,它们就应该被标记为已读。getUnreadCount
: 这个方法用于获取当前用户的未读消息数。未读消息数在用户界面上通常以徽章的形式展示,用于提醒用户有新消息。getUserUnreadMessageInfo
: 这个方法用于获取当前用户与其他用户的未读消息信息列表。返回的结果将用于前端展示用户列表及其未读消息数量。
设计意图:
- 这个接口的设计是为了定义与
chat_info
相关的业务逻辑,并且通过继承 MyBatis Plus 的IService
接口,获得了一些常用的 CRUD 功能。这样,业务逻辑层可以专注于实现与业务相关的操作,而无需关心具体的持久层实现。
- 这个接口的设计是为了定义与
# ChatInfoServiceImpl 实现类
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
@Service
public class ChatInfoServiceImpl extends ServiceImpl<ChatInfoMapper, ChatInfo> implements ChatInfoService {
@Resource
private ChatGroupMapper chatGroupMapper;
@Resource
private ChatInfoMapper chatInfoMapper;
@Override
@Transactional // 使用事务管理,确保数据一致性
public ChatGroup getOrCreateChatGroup(Integer userId, Integer chatUserId) {
QueryWrapper<ChatGroup> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId).eq("chat_user_id", chatUserId);
ChatGroup chatGroup = chatGroupMapper.selectOne(wrapper);
if (chatGroup == null) {
chatGroup = new ChatGroup();
chatGroup.setUserId(userId);
chatGroup.setChatUserId(chatUserId);
chatGroupMapper.insert(chatGroup); // 插入新的聊天组
}
return chatGroup;
}
@Override
public ChatInfo saveChatInfo(ChatInfo chatInfo) {
chatInfo.setTime(DateUtil.now()); // 设置消息发送时间
chatInfo.setIsread("否"); // 设置消息未读状态
chatInfoMapper.insert(chatInfo); // 保存消息到数据库
return chatInfo;
}
@Override
public List<ChatInfo> getChatHistory(Integer userId, Integer chatUserId) {
QueryWrapper<ChatInfo> wrapper = new QueryWrapper<>();
wrapper.and(w -> w.eq("user_id", userId).eq("chat_user_id", chatUserId)
.or()
.eq("user_id", chatUserId).eq("chat_user_id", userId))
.orderByAsc("time"); // 按时间顺序获取聊天记录
return chatInfoMapper.selectList(wrapper);
}
@Override
public void markAsRead(Integer userId, Integer chatUserId) {
QueryWrapper<ChatInfo> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", chatUserId).eq("chat_user_id", userId).eq("isread", "否");
ChatInfo update = new ChatInfo();
update.setIsread("是"); // 将消息标记为已读
chatInfoMapper.update(update, wrapper);
}
@Override
public Long getUnreadCount(Integer userId) {
QueryWrapper<ChatInfo> wrapper = new QueryWrapper<>();
wrapper.eq("chat_user_id", userId).eq("isread", "否");
return chatInfoMapper.selectCount(wrapper); // 返回未读消息数量
}
@Override
public List<UserUnreadMessageDTO> getUserUnreadMessageInfo(String currentUserId) {
return chatInfoMapper.getUserUnreadMessageInfo(currentUserId);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
详细说明:
类名:
ChatInfoServiceImpl
是ChatInfoService
接口的实现类,负责实现所有定义在接口中的业务逻辑方法。这个类使用 Spring 的@Service
注解标识为服务层组件。注解详细说明:
@Service
: 将该类标识为 Spring 的服务层组件,Spring 会自动管理这个类的实例生命周期。@Transactional
: 这个注解标识的方法会在事务管理下执行,确保方法中的数据库操作要么全部成功要么全部回滚,保证数据一致性。
方法详细说明:
getOrCreateChatGroup
: 这个方法首先会根据用户ID和聊天用户ID查询现有的聊天组。如果查询结果为空,说明还没有建立过聊天组,这时会创建一个新的聊天组,并将其保存到数据库中。saveChatInfo
: 这个方法用于保存新的聊天信息。它会自动设置消息的时间和未读状态,并将消息存储到数据库中。getChatHistory
: 这个方法用于获取两个用户之间的聊天历史记录。它会根据user_id
和chat_user_id
的组合查询数据库中的聊天记录,并按时间顺序排序。markAsRead
: 这个方法用于将消息标记为已读。通过更新isread
字段的值,可以避免用户重复收到同一条消息的提醒。getUnreadCount
: 这个方法用于获取当前用户的未读消息数。未读消息数可以用来提醒用户有新的消息未读。getUserUnreadMessageInfo
: 这个方法返回用户的未读消息信息列表,用于前端展示用户的基本信息及其未读消息数量。
设计意图:
- 这个类的设计是为了实现
ChatInfoService
接口中的所有业务逻辑。通过使用 MyBatis Plus 的ServiceImpl
,我们继承了一些常用的基础方法,从而能够专注于实现更复杂的业务逻辑。
- 这个类的设计是为了实现
# 6. Controller 层
# ChatController 控制器
@RestController
@RequestMapping("/chat")
public class ChatController {
@Autowired
private ChatInfoServiceImpl chatService;
/**
* 创建或获取聊天组
* @param userId 当前用户ID
* @param chatUserId 聊天用户ID
* @return 返回创建或获取的聊天组
*/
@PostMapping("/group")
public CommonResult<ChatGroup> createChatGroup(@RequestParam("userId") Integer userId, @RequestParam("chatUserId") Integer chatUserId) {
ChatGroup chatGroup = chatService.getOrCreateChatGroup(userId, chatUserId);
return ResultUtil.success(chatGroup);
}
/**
* 发送消息
* @param chatInfo 消息内容实体类
* @return 返回消息保存成功提示
*/
@PostMapping("/message")
public CommonResult<String> sendMessage(@RequestBody ChatInfo chatInfo) {
chatService.saveChatInfo(chatInfo);
return ResultUtil.success("数据保存成功");
}
/**
* 获取聊天历史
* @param userId 当前用户ID
* @param chatUserId 聊天用户ID
* @return 返回两位用户之间的聊天历史记录
*/
@GetMapping("/history")
public CommonResult<List<ChatInfo>> getChatHistory(@RequestParam("userId") Integer userId, @RequestParam("chatUserId") Integer chatUserId) {
List<ChatInfo> history = chatService.getChatHistory(userId, chatUserId);
return ResultUtil.success(history);
}
/**
* 将消息标记为已读
* @param chatGroup 聊天组实体类,包含当前用户ID和聊天用户ID
* @return 返回操作成功提示
*/
@PutMapping("/read")
public CommonResult<Void> markAsRead(@RequestBody ChatGroup chatGroup) {
chatService.markAsRead(chatGroup.getUserId(), chatGroup.getChatUserId());
return ResultUtil.success();
}
/**
* 获取未读消息数量
* @param userId 当前用户ID
* @return 返回未读消息数量
*/
@GetMapping("/unread/{userId}")
public CommonResult<Long> getUnreadCount(@PathVariable Integer userId) {
Long count = chatService.getUnreadCount(userId);
return ResultUtil.success(count);
}
/**
* 获取用户信息和用户的未读消息数
* @param currentUserId 当前用户ID
* @return 返回用户信息列表及每个用户的未读消息数量
*/
@GetMapping("/getuserinfo/{currentUserId}")
public CommonResult<List<UserUnreadMessageDTO>> getUserInfos(@PathVariable("currentUserId") String currentUserId) {
List<UserUnreadMessageDTO> userinfos = chatService.getUserUnreadMessageInfo(currentUserId);
return ResultUtil.success(userinfos);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
详细说明:
类名:
ChatController
是控制器类,负责处理与聊天功能相关的HTTP请求。这个类中的方法用于响应客户端的请求,并将请求结果以标准化的格式返回给客户端。注解详细说明:
@RestController
: 这个注解将类标识为 Spring MVC 的控制器,并自动将方法的返回值转换为 JSON 格式,返回给客户端。@RequestMapping("/chat")
: 这个注解定义了控制器的请求路径前缀,即所有该类中的请求路径都以/chat
开头。
方法详细说明:
createChatGroup
: 这个方法通过POST
请求来创建或获取聊天组。它接收两个参数:当前用户ID和聊天用户ID,返回创建或获取的聊天组。通过@RequestParam
注解,我们可以从请求中直接获取参数值。sendMessage
: 这个方法用于发送消息。它通过POST
请求接收一个ChatInfo
对象,保存消息后返回操作结果。getChatHistory
: 这个方法用于获取两位用户之间的聊天历史。通过GET
请求传递用户ID和聊天用户ID,返回两人之间的聊天记录列表。markAsRead
: 这个方法用于将消息标记为已读。它接收一个ChatGroup
对象(包含用户ID和聊天用户ID),并通过PUT
请求将消息状态更新为已读。getUnreadCount
: 这个方法用于获取当前用户的未读消息数量。它通过GET
请求传递用户ID,返回未读消息数。getUserInfos
: 这个方法用于获取用户的基本信息和未读消息数。它通过GET
请求传递当前用户ID,返回一个包含用户信息和未读消息数的列表。
设计意图:
- 这个控制器类的设计目的是将服务层的业务逻辑暴露为 RESTful API,以供前端调用。每个方法都对应一个具体的业务操作,并通过标准的 HTTP 方法来区分不同的操作类型(如
GET
、POST
、PUT
等)。
- 这个控制器类的设计目的是将服务层的业务逻辑暴露为 RESTful API,以供前端调用。每个方法都对应一个具体的业务操作,并通过标准的 HTTP 方法来区分不同的操作类型(如
# 7. WebSocket 配置和处理器
# ChatWebSocket 类
import com.fasterxml.jackson.databind.ObjectMapper;
import com.example.chat.entity.ChatInfo;
import com.example.chat.service.impl.ChatInfoServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/ws/chat/{userId}") // 指定 WebSocket 连接的 URL
@Component
public class ChatWebSocket {
private static final Logger logger = LoggerFactory.getLogger(ChatWebSocket.class);
private static final Map<Integer, Session> sessionMap = new ConcurrentHashMap<>(); // 用于存储用户的 WebSocket 会话
private static final ObjectMapper objectMapper = new ObjectMapper(); // 用于 JSON 解析和生成
private static ChatInfoServiceImpl chatService;
@Autowired
public void setChatService(ChatInfoServiceImpl chatService) {
ChatWebSocket.chatService = chatService;
}
@OnOpen
public void onOpen(Session session, @PathParam("userId") Integer userId) {
sessionMap.put(userId, session); // 将用户的会话存储到 sessionMap 中
logger.info("用户 {} 已连接 WebSocket", userId);
}
@OnClose
public void onClose(@PathParam("userId") Integer userId) {
sessionMap.remove(userId); // 用户断开连接时移除其会话
logger.info("用户 {} 已断开 WebSocket 连接", userId);
}
@OnMessage
public void onMessage(String message, @PathParam("userId") Integer userId) throws IOException {
logger.info("接收到 WebSocket 消息,用户ID: {},消息内容: {}", userId, message);
ChatInfo chatInfo = objectMapper.readValue(message, ChatInfo.class); // 将收到的 JSON 消息解析为 ChatInfo 对象
ChatInfo savedChatInfo = chatService.saveChatInfo(chatInfo); // 保存消息并获取完整的消息对象
String savedMessage = objectMapper.writeValueAsString(savedChatInfo); // 将完整的消息对象转换为 JSON 字符串
Session receiverSession = sessionMap.get(chatInfo.getChatUserId()); // 获取接收方的会话
if (receiverSession != null && receiverSession.isOpen()) {
receiverSession.getBasicRemote().sendText(savedMessage); // 将消息发送给接收方
}
Session senderSession = sessionMap.get(userId); // 获取发送方的会话
if (senderSession != null && senderSession.isOpen()) {
senderSession.getBasicRemote().sendText(savedMessage); // 将消息发送给发送方(自己),确认消息已发送
}
}
@OnError
public void onError(Session session, Throwable error) {
logger.error("WebSocket 发生错误: {}", error.getMessage(), error);
}
public static void sendMessage(Integer userId, String message) throws IOException {
Session session = sessionMap.get(userId); // 获取指定用户的 WebSocket 会话
if (session != null && session.isOpen()) {
session.getBasicRemote().sendText(message); // 发送消息
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
详细说明:
类名:
ChatWebSocket
是处理 WebSocket 连接和消息的类。通过这个类,我们可以在聊天过程中实现实时的消息传输。注解详细说明:
@ServerEndpoint("/ws/chat/{userId}")
: 这个注解标识了 WebSocket 端点的路径,并通过{userId}
来动态绑定用户ID。每个用户都会有一个独立的 WebSocket 连接。@Component
: 这个注解将ChatWebSocket
类注册为 Spring 的组件,方便依赖注入。
字段详细说明:
sessionMap
: 这是一个线程安全的ConcurrentHashMap
,用于存储当前在线用户的 WebSocket 会话。通过用户ID,我们可以找到对应的会话,以便在发送消息时使用。objectMapper
: 这是 Jackson 提供的ObjectMapper
实例,用于在 JSON 和 Java 对象之间进行转换。这个工具非常适合在 WebSocket 传输中使用,因为消息通常是以 JSON 格式传递的。
方法详细说明:
onOpen
: 这个方法在 WebSocket 连接建立时调用。它会将用户的 WebSocket 会话存储到sessionMap
中,并记录日志。onClose
: 这个方法在 WebSocket 连接关闭时调用。它会从sessionMap
中移除用户的会话,并记录日志。onMessage
: 这个方法在收到消息时调用。它首先将 JSON 格式的消息转换为ChatInfo
对象,然后保存消息,并将消息发送给接收方和发送方(即自己),以确保消息已发送成功。onError
: 这个方法在 WebSocket 连接发生错误时调用。它记录错误信息,方便排查问题。sendMessage
: 这个静态方法允许我们主动向指定用户发送消息。通过获取用户的会话并调用sendText
方法,可以将消息发送给用户。
设计意图:
- 这个类的设计是为了处理聊天系统中的实时消息传输。通过 WebSocket,我们可以实现用户之间的实时通讯,而不用依赖传统的 HTTP 请求。这对于聊天应用来说是一个关键的功能。
# WebSocket 配置类
package com.example.chat.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter(); // 自动注册使用 @ServerEndpoint 注解的 WebSocket 端点
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
详细说明:
类名:
WebSocketConfig
是 WebSocket 的配置类。它的主要职责是配置和注册 WebSocket 端点。注解详细说明:
@Configuration
: 这个注解标识了该类为一个配置类,Spring 会自动扫描并加载这个配置类中的Bean
定义。@Bean
: 这个注解标识了serverEndpointExporter
方法作为一个Bean
,Spring 会自动将其实例化并管理。
方法详细说明:
serverEndpointExporter
: 这个方法返回一个ServerEndpointExporter
对象。ServerEndpointExporter
是 Spring 提供的一个类,它会自动注册使用 `
@ServerEndpoint` 注解的 WebSocket 端点。
- 设计意图:
- 这个类的设计目的是为了配置 WebSocket 环境。通过配置
ServerEndpointExporter
,Spring 可以自动处理和注册 WebSocket 端点,使得开发者只需要专注于编写具体的业务逻辑,而不需要手动配置 WebSocket 环境。
- 这个类的设计目的是为了配置 WebSocket 环境。通过配置
# 8. 前端代码
# Vue 组件:聊天界面
<template>
<el-container class="chat-container">
<!-- 左侧用户列表部分 -->
<el-aside width="250px" class="user-list">
<!-- 搜索框,用于过滤用户列表 -->
<el-input
v-model="searchQuery"
placeholder="搜索用户"
prefix-icon="el-icon-search"
></el-input>
<!-- 用户列表菜单 -->
<el-menu :default-active="activeChat.toString()">
<el-menu-item
v-for="user in filteredUsers"
:key="user.id"
:index="user.id.toString()"
@click="selectUser(user)"
>
<!-- 显示未读消息数 -->
<el-badge :value="user.unreadCount" :max="99" :hidden="!user.unreadCount">
<el-avatar :src="user.avatar" size="small"></el-avatar>
</el-badge>
<span>{{ user.username }}</span>
</el-menu-item>
</el-menu>
</el-aside>
<!-- 右侧聊天区域 -->
<el-main class="chat-main">
<template v-if="selectedUser">
<!-- 聊天窗口顶部,显示当前聊天用户 -->
<div class="chat-header">
<h3>{{ selectedUser.username }}</h3>
</div>
<!-- 消息列表 -->
<div class="message-list" ref="messageList">
<div
v-for="message in chatMessages"
:key="message.id"
:class="['message', { 'message-self': message.userId === currentUser.id }]"
>
<!-- 消息的头像,根据消息发送者决定 -->
<el-avatar :src="message.userId === currentUser.id ? currentUser.avatar : selectedUser.avatar" size="small"></el-avatar>
<div class="message-content">
<!-- 消息文本内容 -->
<p>{{ message.text }}</p>
<!-- 消息发送时间 -->
<span class="message-time">{{ formatTime(message.time) }}</span>
</div>
</div>
</div>
<!-- 消息输入框 -->
<div class="message-input">
<el-input
v-model="newMessage"
placeholder="输入消息"
@keyup.enter.native="sendMessage"
>
<!-- 发送按钮 -->
<el-button slot="append" @click="sendMessage" icon="el-icon-s-promotion">发送</el-button>
</el-input>
</div>
</template>
<!-- 当没有选中用户时,显示提示信息 -->
<el-empty v-else description="选择一个用户开始聊天"></el-empty>
</el-main>
</el-container>
</template>
<script>
import request from "@/request";
export default {
name: 'Chat',
data() {
return {
currentUser: null, // 当前登录的用户信息
users: [], // 用户列表
selectedUser: null, // 当前选中的聊天用户
chatMessages: [], // 当前聊天的消息列表
newMessage: '', // 新消息的内容
searchQuery: '', // 用户搜索查询
socket: null, // WebSocket 实例
};
},
computed: {
// 过滤后的用户列表,基于搜索查询
filteredUsers() {
return this.users.filter(user =>
user.username.toLowerCase().includes(this.searchQuery.toLowerCase())
);
},
// 当前活动的聊天,返回选中用户的 ID
activeChat() {
return this.selectedUser ? this.selectedUser.id : '';
},
},
methods: {
// 获取当前登录用户的信息
getCurrentUser() {
const userInfo = this.$store.state.user;
if (userInfo) {
this.currentUser = userInfo;
} else {
// 如果没有用户信息,重定向到登录页面
this.$router.push('/login');
}
},
// 从服务器获取用户列表
async fetchUsers() {
try {
const response = await request.get(`/chat/getuserinfo/${this.$store.state.user.id}`);
// 过滤掉当前用户,显示其他用户
this.users = response.data.filter(user => user.id !== this.currentUser.id);
} catch (error) {
console.error('获取用户列表失败:', error);
this.$message.error('获取用户列表失败');
}
},
// 选择一个用户进行聊天
async selectUser(user) {
this.selectedUser = user;
await this.fetchChatHistory(); // 获取选中用户的聊天记录
await this.markAsRead(); // 将消息标记为已读
},
// 从服务器获取当前选中用户的聊天记录
async fetchChatHistory() {
try {
const response = await request.get('/chat/history', {
params: {
userId: this.currentUser.id,
chatUserId: this.selectedUser.id
}
});
this.chatMessages = response.data;
this.$nextTick(() => {
this.scrollToBottom(); // 滚动到消息列表底部
});
} catch (error) {
console.error('获取聊天记录失败:', error);
this.$message.error('获取聊天记录失败');
}
},
// 发送新消息
async sendMessage() {
if (!this.newMessage.trim()) return; // 如果消息为空,则不发送
const message = {
userId: this.currentUser.id, // 发送者 ID
chatUserId: this.selectedUser.id, // 接收者 ID
text: this.newMessage, // 消息内容
};
try {
// 通过 WebSocket 发送消息
this.socket.send(JSON.stringify(message));
this.newMessage = ''; // 清空输入框
this.scrollToBottom(); // 滚动到消息列表底部
} catch (error) {
console.error('发送消息失败:', error);
this.$message.error('发送消息失败');
}
},
// 将消息标记为已读
async markAsRead() {
try {
await request.put('/chat/read', {
userId: this.currentUser.id,
chatUserId: this.selectedUser.id
});
this.updateUnreadCount(this.selectedUser.id, 0); // 更新未读消息数为 0
} catch (error) {
console.error('标记已读失败:', error);
}
},
// 更新用户的未读消息数
updateUnreadCount(userId, count) {
const user = this.users.find(u => u.id === userId);
if (user) {
this.$set(user, 'unreadCount', count);
}
},
// 初始化 WebSocket 连接
initWebSocket() {
this.socket = new WebSocket(`ws://localhost:3000/ws/chat/${this.currentUser.id}`);
this.socket.onopen = () => {
this.$message.success('WebSocket连接已建立');
};
this.socket.onmessage = (event) => {
const message = JSON.parse(event.data);
// 检查消息是否发送给当前客户端的用户
if (message.chatUserId === this.currentUser.id) {
// 如果用户没有选中聊天窗口,或选中的不是消息发送者的窗口,增加未读计数
if (!this.selectedUser || this.selectedUser.id !== message.userId) {
this.updateUnreadCount(message.userId, (this.users.find(u => u.id === message.userId)?.unreadCount || 0) + 1);
} else {
// 如果用户正在查看发送者的聊天窗口,标记为已读
this.markAsRead();
}
}
// 检查消息是否与当前选中的聊天对象相关(无论是发送还是接收),都显示在消息列表中
if (message.chatUserId === this.selectedUser?.id || message.userId === this.selectedUser?.id) {
this.chatMessages.push(message); // 添加消息到列表
this.scrollToBottom(); // 滚动到消息列表底部
}
};
this.socket.onclose = () => {
this.$message.error('WebSocket连接已关闭');
console.log('WebSocket连接已关闭');
};
this.socket.onerror = (error) => {
console.error('WebSocket错误:', error);
this.$message.error('WebSocket错误: ' + error.message);
};
},
// 滚动消息列表到最底部
scrollToBottom() {
this.$nextTick(() => {
const container = this.$refs.messageList;
if (container) {
container.scrollTop = container.scrollHeight;
}
});
},
// 格式化消息时间
formatTime(time) {
return new Date(time).toLocaleString();
},
},
created() {
this.getCurrentUser(); // 获取当前用户信息
if (this.currentUser) {
this.fetchUsers(); // 获取用户列表
this.initWebSocket(); // 初始化
WebSocket 连接
}
},
beforeDestroy() {
if (this.socket) {
this.socket.close(); // 在组件销毁前关闭 WebSocket 连接
}
},
};
</script>
<style scoped>
.chat-container {
height: 100vh;
}
.user-list {
border-right: 1px solid #dcdfe6;
overflow-y: auto;
}
.chat-main {
display: flex;
flex-direction: column;
padding: 0;
}
.chat-header {
padding: 10px;
border-bottom: 1px solid #dcdfe6;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
display: flex;
margin-bottom: 15px;
}
.message-self {
flex-direction: row-reverse;
}
.message-content {
max-width: 70%;
padding: 10px;
border-radius: 4px;
background-color: #f4f4f5;
margin: 0 10px;
}
.message-self .message-content {
background-color: #e1f3d8;
}
.message-time {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.message-input {
padding: 10px;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
详细说明:
组件名:
Chat
是一个 Vue 组件,专门用于实现前端的聊天界面。这个组件包括用户列表、消息列表、消息输入框等关键部分,能够实现基本的聊天功能。模板部分详细说明:
el-container
: 这是一个布局容器组件,将整个聊天界面分为左右两部分。左侧是用户列表,右侧是聊天区域。el-aside
: 这个组件用于容纳左侧的用户列表。通过width="250px"
设置了固定宽度,以确保用户列表的布局稳定。el-input
: 用于实现用户列表的搜索功能。通过v-model
绑定到searchQuery
数据属性,用户输入的内容会实时更新searchQuery
,从而实现搜索过滤功能。el-menu
和el-menu-item
: 用于展示用户列表,每个用户都是一个el-menu-item
。通过v-for
循环遍历用户数据并动态生成菜单项。每个菜单项都显示用户名和未读消息数,并通过点击事件触发用户选择操作。el-main
: 这个组件用于容纳右侧的聊天区域。当用户选择了一个聊天对象后,会在这里显示聊天记录和消息输入框。el-avatar
和el-badge
: 用于在用户列表和聊天记录中显示用户的头像和未读消息数。头像和未读消息数在聊天界面中具有重要的视觉引导作用。el-input
: 用于实现消息输入框。通过v-model
绑定到newMessage
数据属性,用户输入的内容会实时更新newMessage
,并且通过@keyup.enter.native
监听回车键事件实现消息发送功能。el-empty
: 当没有选中聊天对象时,显示一个提示信息,引导用户选择一个聊天对象。
脚本部分详细说明:
data
: 定义了组件的状态数据,包括当前用户信息、用户列表、选中聊天用户、聊天消息、新消息内容、搜索查询、WebSocket 实例等。这些数据构成了整个聊天界面的核心。computed
: 计算属性用于在不修改数据的情况下,动态计算并返回需要的数据。例如filteredUsers
会根据搜索查询过滤用户列表,activeChat
返回当前选中用户的ID,用于高亮显示用户列表中的选中项。methods
:getCurrentUser
: 获取当前登录用户的信息,并确保用户已登录。如果未登录,则重定向到登录页面。fetchUsers
: 从服务器获取用户列表,并过滤掉当前用户。这一步确保聊天列表中不显示自己。selectUser
: 选择一个用户进行聊天,并获取该用户的聊天记录,随后将该用户的消息标记为已读。fetchChatHistory
: 从服务器获取当前选中用户的聊天记录,并在消息列表中显示出来。sendMessage
: 发送新消息。消息内容通过 WebSocket 实时传输给服务器,并显示在当前聊天窗口中。markAsRead
: 将当前选中用户的消息标记为已读,并更新未读消息数。updateUnreadCount
: 更新用户的未读消息数。这个方法会在消息接收后调用,用于确保界面上显示的未读消息数是最新的。initWebSocket
: 初始化 WebSocket 连接,并处理连接的各种事件(如消息接收、连接关闭、错误处理等)。这是实现实时聊天的关键。scrollToBottom
: 滚动消息列表到最底部,确保用户能够看到最新的消息。formatTime
: 格式化消息时间,使其以用户友好的方式显示在界面上。
生命周期钩子函数详细说明:
created
: 在组件创建时,获取当前用户信息、用户列表,并初始化 WebSocket 连接。这是确保聊天界面正常工作的基础。beforeDestroy
: 在组件销毁前关闭 WebSocket 连接,以防止内存泄漏和不必要的连接保留。
样式部分详细说明:
chat-container
: 定义了整个聊天界面的高度,使其占满屏幕。user-list
: 设置了用户列表的样式,包括右侧的边框和滚动条。chat-main
: 定义了聊天区域的布局,使其以列方向布局,并填充满容器。chat-header
: 定义了聊天窗口顶部的样式,包括用户名的显示和下边框。message-list
: 设置了消息列表的滚动条样式和内边距,确保消息内容在视觉上整齐。message
: 定义了消息项的样式,包括发送者头像和消息内容的布局。message-self
: 定义了当前用户发送的消息样式,使其在界面上靠右显示,并使用不同的背景色。message-content
: 定义了消息文本内容的样式,使其背景色与正常文本有所区别,并设置了圆角。message-time
: 定义了消息时间的样式,使其显示在消息内容下方,并使用较小的字体。message-input
: 定义了消息输入框的样式,包括内边距和布局。