SpringBoot 聊天消息持久化
单聊 - 实现功能
- 发送文本消息
- 聊天列表
- 聊天消息详情
通过上一篇 SpringBoot 集成 netty-socketio 客户端可以把消息发送给服务端,服务端也可以正常接收
将接收的消息进行持久化存储,使用 mysql 数据库进行数据存储,mybatis 操作数据库
对数据库不了解,可以看这
聊天消息列表存储
创建消息记录列表表
CREATE TABLE `t_message_record` (
`msg_id` bigint(20) NOT NULL COMMENT '消息Id',
`send_id` bigint(20) NOT NULL COMMENT '发送者Id',
`receive_id` bigint(20) NOT NULL COMMENT '接受者Id',
`type` char(1) NOT NULL COMMENT '消息类型',
`chat_type` char(1) NOT NULL COMMENT '聊天类型',
`content` varchar(500) NOT NULL COMMENT '聊天内容',
`send_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '消息发送时间',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`msg_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='消息记录表';
编写 MessageRecordMapper 操作数据库
/**
* @author taohua
*/
@Mapper
public interface MessageRecordMapper {
/**
* 单聊
* <p>
* 插入一条单聊消息
*
* @param messageRecordDO 消息实体
* @return 添加成功
*/
int insertMessageRecord(MessageRecordDO messageRecordDO);
}
编写 MessageRecordMapper 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.moose.operator.mapper.MessageRecordMapper">
<insert id="insertMessageRecord"
parameterType="com.moose.operator.model.domain.message.MessageRecordDO">
INSERT INTO `t_message_record` (msg_id, send_id, receive_id, type, chat_type, content)
VALUES (#{msgId}, #{sendId}, #{receiveId}, #{type}, #{chatType}, #{content})
</insert>
</mapper>
在 SingleMessageServiceImpl 中使用
saveMessage主要逻辑
@Resource
private SocketConnection socketConnection;
// 注入 MessageRecordMapper
@Resource
private MessageRecordMapper messageRecordMapper;
@Override public void saveMessage(MessageTemplate template) {
// TODO: 消息 ack
MessageRecordDO messageRecord = null;
try {
// mock
//String fromUserId = template.getFromUserId();
//SocketIOClient fromSocket = socketIOClientMap.get(fromUserId);
//if ("786600935907659776".equals(toUserId)) {
// if (null != fromSocket && fromSocket.isChannelOpen()) {
// fromSocket.sendEvent(ChatConstant.SINGLE_CHAT, "你们不是好友!!");
// }
// return;
//}
//log.info("{}", request);
// 检查 消息模版 参数 ...
// 判断用户是否存在 ...
// 是否是好友 ...
/////////////////////////// save message record ///////////////////////////
// 发送者ID
Long sendId = template.getSendId();
// 接受者 ID
Long receiveId = template.getReceiveId();
// 构建消息
messageRecord = new MessageRecordDO();
messageRecord.setMsgId(snowflakeIdWorker.nextId());
messageRecord.setSendId(sendId);
messageRecord.setReceiveId(receiveId);
messageRecord.setContent(template.getContent());
messageRecord.setType(MessageUtils.diffMessageType(template.getType()));
messageRecord.setChatType(MessageUtils.diffChatType(template.getChatType()));
// 保存消息
messageRecordMapper.insertMessageRecord(messageRecord);
} catch (Exception exception) {
exception.printStackTrace();
log.info("消息解析: 错误 [{}] 消息 [{}]", exception.getMessage(), template);
}
}
服务单元测试用例
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class SingleMessageServiceTest {
@Resource
private SingleMessageService singleMessageService;
@Test
public void testSaveMessage() {
MessageTemplate template = new MessageTemplate();
mockJiangJing(template);
mockTaohua(template);
mockWangZhaoJun(template);
}
private void mockWangZhaoJun(MessageTemplate template) {
// 王昭君
template.setSendId(775113183131074560L);
// 江景
template.setReceiveId(775113183131074580L);
template.setType("MS:TEXT");
template.setChatType("CT:SINGLE");
template.setContent("你好,江景,我是王昭君。");
singleMessageService.saveMessage(template);
}
private void mockTaohua(MessageTemplate template) {
// 桃花
template.setSendId(786600935907659776L);
// 王昭君
template.setReceiveId(775113183131074560L);
template.setType("MS:TEXT");
template.setChatType("CT:SINGLE");
template.setContent("你好,王昭君,我是桃花。");
singleMessageService.saveMessage(template);
}
private void mockJiangJing(MessageTemplate template) {
// 江景
template.setSendId(775113183131074580L);
// 桃花
template.setReceiveId(786600935907659776L);
template.setType("MS:TEXT");
template.setChatType("CT:SINGLE");
template.setContent("你好,桃花,我是江景。");
singleMessageService.saveMessage(template);
// 王昭君
template.setReceiveId(775113183131074560L);
template.setType("MS:TEXT");
template.setChatType("CT:SINGLE");
template.setContent("你好,王昭君,我是江景。");
singleMessageService.saveMessage(template);
}
}
单聊实现
- 消息保存之后再发送消息??
- 从
socketConnection中获取,需要判断时候在线 - 使用 SocketIOClient sendEvent 发送消息
代码
// TODO: 消息传输过程失败情况
// TODO: 并发情况
// 发送消息
// 判断是否在线
SocketIOClient receiveSocket = socketConnection.getSocketIOClient(String.valueOf(receiveId));
if (null != receiveSocket && receiveSocket.isChannelOpen()) {
// 客户端监听单聊事件
receiveSocket.sendEvent(ChatConstant.SINGLE_CHAT, MapperUtils.obj2json(template));
// TODO: un read count compute --> 0
// messageRecord.setUnReadCount(0);
} else {
// TODO: un read count compute --> redis increment or decrement
// messageRecord.setUnReadCount(0);
}
调试
先使用
socket_io_client调试; dart 语言模块, 后面集成 Flutter 方便
具体代码参考 gitee.com/shizidada/d…
实现效果
已经长连接,接受到数据可以在 UI 上进行渲染
获取聊天列表
MessageRecordMapper java 添加查询方法
/**
* 聊天列表
*
* @param userId 当前用户
* @return 聊天列表
*/
List<MessageRecordDO> selectMessageRecordList(Long userId);
/**
* 查询聊天列表详情
*
* @param sendId 发送者 Id
* @param receiveId 接收者 Id
* @return 聊天详情
*/
List<MessageRecordDO> selectChatMessageList(@Param("sendId") Long sendId,
@Param("receiveId") Long receiveId);
MessageRecordMapper xml 添加查询映射
<select id="selectMessageRecordList" resultMap="BaseResultMap">
<![CDATA[
SELECT t.* FROM (
SELECT
msg_id,
send_id,
receive_id,
type,
chat_type,
content,
send_time
FROM (
SELECT
receive_id AS receiver,
msg_id,
send_id,
receive_id,
type,
chat_type,
content,
send_time
FROM
t_message_record
WHERE
send_id = #{userId} AND receive_id <> #{userId}
UNION
SELECT
send_id AS receiver,
msg_id,
send_id,
receive_id,
type,
chat_type,
content,
send_time
FROM
t_message_record
WHERE
send_id <> #{userId} AND receive_id = #{userId}
ORDER BY send_time DESC ) AS newTable
GROUP BY
receiver
ORDER BY
send_time DESC
) AS t
]]>
</select>
<select id="selectChatMessageList" resultMap="BaseResultMap">
SELECT
<include refid="Base_Column"/>
FROM
t_message_record
WHERE
( send_id = #{sendId} AND receive_id = #{receiveId} )
OR
( send_id = #{receiveId} AND receive_id = #{sendId} )
ORDER BY send_time ASC
</select>
单元测试
@Test
public void testSelectMessageRecordList() {
// 江景
Long userId = 775113183131074580L;
List<MessageRecordDO> messageRecordDOS = messageRecordMapper.selectMessageRecordList(userId);
log.info("{}", messageRecordDOS);
}
编写接口
获取聊天列表服务 MessageRecordService
public interface MessageRecordService {
/**
* 获取聊天消息列表
*
* @return List<ChatMessageRecord>
*/
List<MessageRecordDO> listMessageRecord();
/**
* 查询聊天列表
*
* @param userId 对方 Id
* @param pageNum 页码
* @return 聊天列表详细
*/
List<MessageRecordDO> listMessageChat(Long userId, Integer pageNum);
}
服务实现 MessageRecordServiceImpl
@Slf4j
@Service
public class MessageRecordServiceImpl implements MessageRecordService {
@Resource
private UserInfoService userInfoService;
@Resource
private MessageRecordMapper messageRecordMapper;
@Override public List<MessageRecordDO> listMessageRecord() {
Long userId = userInfoService.getCurrentUserId();
return messageRecordMapper.selectMessageRecordList(userId);
}
@Override public List<MessageRecordDO> listMessageChat(Long userId, Integer pageNum) {
Long currentUserId = userInfoService.getCurrentUserId();
PageHelper.startPage(pageNum, 10);
return messageRecordMapper.selectChatMessageList(currentUserId, userId);
}
}
PageHelper 进行分页查询, 根据分页一次只查询 10 条数据
PageHelper.startPage(pageNum, 10);
消息接口 MessageController
@RestController
@RequestMapping("/api/v1/message")
public class MessageController {
@Resource
private MessageRecordService messageRecordService;
/**
* 获取当前登录用户聊天列表
*
* @return R<?> 聊天列表
*/
@PostMapping(value = "/record/list")
public R<?> getChatMessageList() {
return R.ok(messageRecordService.listMessageRecord());
}
/**
* 获取单聊记录 对方 Id
*
* @return R<?> 聊天记录
*/
@PostMapping(value = "/chat/list")
public R<?> getMessageChatList(
@RequestParam("chatId") Long chatId,
@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum
) {
return R.ok(messageRecordService.listMessageChat(chatId, pageNum));
}
}
启动服务访问
获取聊天记录列表 http://localhost:7000/api/v1/message/record/list
获取对应聊天记录列表 http://localhost:7000/api/v1/message/chat/list
TODO 待完善
- 群聊、发送二进制消息(图片消息、视频消息...)
- 检查消息模版参数
- 判断用户是否存在
- 是否是好友
- 主动推送?还是拉去消息列表
- 消息存储
读扩散,写扩散 - 分布式情况下,消息推送
- ....
关注公众号 「全栈技术部」,不断学习更多有趣的技术知识。