392 lines
8.8 KiB
Markdown
392 lines
8.8 KiB
Markdown
|
|
# 离线消息功能实现说明
|
|||
|
|
|
|||
|
|
## 📋 功能概述
|
|||
|
|
|
|||
|
|
离线消息服务是直播IM系统的核心功能之一,用于在用户离线时保存消息,并在用户重新上线时推送给用户。
|
|||
|
|
|
|||
|
|
## 🎯 核心功能
|
|||
|
|
|
|||
|
|
### 1. 消息保存
|
|||
|
|
- 当用户离线时,系统自动将发送给该用户的消息保存到Redis队列中
|
|||
|
|
- 每个用户最多保存100条离线消息(可配置)
|
|||
|
|
- 离线消息保存7天后自动过期(可配置)
|
|||
|
|
|
|||
|
|
### 2. 消息推送
|
|||
|
|
- 用户上线时,系统自动推送所有离线消息
|
|||
|
|
- 推送完成后自动清除已推送的离线消息
|
|||
|
|
|
|||
|
|
### 3. 消息管理
|
|||
|
|
- 获取离线消息数量
|
|||
|
|
- 获取离线消息列表(支持分页)
|
|||
|
|
- 删除指定数量的离线消息
|
|||
|
|
- 清空所有离线消息
|
|||
|
|
|
|||
|
|
## 🏗️ 技术架构
|
|||
|
|
|
|||
|
|
### 数据存储
|
|||
|
|
|
|||
|
|
使用Redis List存储离线消息:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Key格式: offline:msg:{userId}
|
|||
|
|
数据结构: List
|
|||
|
|
过期时间: 7天
|
|||
|
|
最大长度: 100条
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 核心类说明
|
|||
|
|
|
|||
|
|
#### 1. OfflineMessageService (接口)
|
|||
|
|
定义离线消息服务的所有方法:
|
|||
|
|
- `saveOfflineMessage()` - 保存离线消息
|
|||
|
|
- `getOfflineMessages()` - 获取离线消息(分页)
|
|||
|
|
- `getAllOfflineMessages()` - 获取所有离线消息
|
|||
|
|
- `clearOfflineMessages()` - 清空离线消息
|
|||
|
|
- `getOfflineMessageCount()` - 获取消息数量
|
|||
|
|
- `removeOfflineMessages()` - 删除指定数量的消息
|
|||
|
|
|
|||
|
|
#### 2. OfflineMessageServiceImpl (实现类)
|
|||
|
|
实现离线消息服务的具体逻辑:
|
|||
|
|
- 使用RedisUtil操作Redis
|
|||
|
|
- 自动添加时间戳
|
|||
|
|
- 限制消息数量
|
|||
|
|
- 设置过期时间
|
|||
|
|
|
|||
|
|
#### 3. OfflineMessageController (控制器)
|
|||
|
|
提供HTTP接口:
|
|||
|
|
- `GET /api/front/offline-messages/count/{userId}` - 获取消息数量
|
|||
|
|
- `GET /api/front/offline-messages/list/{userId}` - 获取消息列表
|
|||
|
|
- `GET /api/front/offline-messages/all/{userId}` - 获取所有消息
|
|||
|
|
- `DELETE /api/front/offline-messages/clear/{userId}` - 清空消息
|
|||
|
|
- `DELETE /api/front/offline-messages/remove/{userId}` - 删除指定数量
|
|||
|
|
- `POST /api/front/offline-messages/save` - 保存消息(测试接口)
|
|||
|
|
|
|||
|
|
#### 4. OfflineMessageCleanupTask (定时任务)
|
|||
|
|
定期清理和监控离线消息:
|
|||
|
|
- 每天凌晨3点统计离线消息使用情况
|
|||
|
|
- 每小时检查离线消息健康状况
|
|||
|
|
- 发现异常大的消息队列时发出告警
|
|||
|
|
|
|||
|
|
## 🔄 工作流程
|
|||
|
|
|
|||
|
|
### 消息发送流程
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
1. 用户A发送消息给用户B
|
|||
|
|
↓
|
|||
|
|
2. 检查用户B是否在线
|
|||
|
|
↓
|
|||
|
|
3. 如果在线 → 直接推送消息
|
|||
|
|
↓
|
|||
|
|
4. 如果离线 → 保存到离线消息队列
|
|||
|
|
↓
|
|||
|
|
5. 用户B上线时 → 自动推送离线消息
|
|||
|
|
↓
|
|||
|
|
6. 推送完成 → 清除已推送的消息
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### WebSocket集成
|
|||
|
|
|
|||
|
|
在`PrivateChatHandler`中已经集成了离线消息功能:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Override
|
|||
|
|
public void afterConnectionEstablished(WebSocketSession session) {
|
|||
|
|
// ... 连接建立逻辑 ...
|
|||
|
|
|
|||
|
|
// 推送离线消息
|
|||
|
|
List<String> offlineMessages = offlineMessageService.getAllOfflineMessages(userId);
|
|||
|
|
if (!offlineMessages.isEmpty()) {
|
|||
|
|
for (String offlineMsg : offlineMessages) {
|
|||
|
|
sendToSession(session, offlineMsg);
|
|||
|
|
}
|
|||
|
|
// 清除已推送的离线消息
|
|||
|
|
offlineMessageService.clearOfflineMessages(userId);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
在消息发送时检查用户是否在线:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 检查对方是否在线
|
|||
|
|
if (onlineStatusService.isUserOnline(otherUserId)) {
|
|||
|
|
// 在线则直接推送
|
|||
|
|
notifyUser(otherUserId, buildNewMessageNotification(conversationId, response));
|
|||
|
|
} else {
|
|||
|
|
// 离线则保存为离线消息
|
|||
|
|
offlineMessageService.saveOfflineMessage(otherUserId, chatMsg);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 📊 Redis数据结构
|
|||
|
|
|
|||
|
|
### 离线消息队列
|
|||
|
|
|
|||
|
|
```redis
|
|||
|
|
# Key格式
|
|||
|
|
offline:msg:{userId}
|
|||
|
|
|
|||
|
|
# 数据类型
|
|||
|
|
List
|
|||
|
|
|
|||
|
|
# 示例数据
|
|||
|
|
LRANGE offline:msg:1 0 -1
|
|||
|
|
1) "{\"type\":\"chat\",\"content\":\"你好\",\"timestamp\":1234567890,\"savedAt\":1234567900}"
|
|||
|
|
2) "{\"type\":\"chat\",\"content\":\"在吗\",\"timestamp\":1234567891,\"savedAt\":1234567901}"
|
|||
|
|
|
|||
|
|
# 过期时间
|
|||
|
|
TTL offline:msg:1
|
|||
|
|
604800 # 7天 = 7 * 24 * 3600秒
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 消息格式
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "chat",
|
|||
|
|
"messageId": "msg_123456",
|
|||
|
|
"userId": 2,
|
|||
|
|
"username": "张三",
|
|||
|
|
"avatarUrl": "http://example.com/avatar.jpg",
|
|||
|
|
"message": "你好,在吗?",
|
|||
|
|
"timestamp": 1234567890,
|
|||
|
|
"savedAt": 1234567900,
|
|||
|
|
"status": "sent"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 🔧 配置说明
|
|||
|
|
|
|||
|
|
### 可配置参数
|
|||
|
|
|
|||
|
|
在`OfflineMessageServiceImpl`中:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 离线消息最大保存数量(每个用户)
|
|||
|
|
private static final int MAX_OFFLINE_MESSAGES = 100;
|
|||
|
|
|
|||
|
|
// 离线消息过期时间(秒)- 7天
|
|||
|
|
private static final long OFFLINE_MESSAGE_EXPIRE_SECONDS = 7 * 24 * 3600;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 修改配置
|
|||
|
|
|
|||
|
|
如果需要修改配置,可以:
|
|||
|
|
|
|||
|
|
1. 直接修改常量值
|
|||
|
|
2. 或者改为从配置文件读取:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Value("${offline.message.max-count:100}")
|
|||
|
|
private int maxOfflineMessages;
|
|||
|
|
|
|||
|
|
@Value("${offline.message.expire-seconds:604800}")
|
|||
|
|
private long offlineMessageExpireSeconds;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
然后在`application.yml`中配置:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
offline:
|
|||
|
|
message:
|
|||
|
|
max-count: 100 # 最大保存数量
|
|||
|
|
expire-seconds: 604800 # 过期时间(7天)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 🧪 测试方法
|
|||
|
|
|
|||
|
|
### 1. 使用测试HTML页面
|
|||
|
|
|
|||
|
|
打开`测试离线消息功能.html`文件,可以测试:
|
|||
|
|
- 保存离线消息
|
|||
|
|
- 获取离线消息
|
|||
|
|
- 删除离线消息
|
|||
|
|
- 清空离线消息
|
|||
|
|
|
|||
|
|
### 2. 使用Postman测试
|
|||
|
|
|
|||
|
|
#### 保存离线消息
|
|||
|
|
```http
|
|||
|
|
POST http://localhost:8081/api/front/offline-messages/save?userId=1
|
|||
|
|
Content-Type: application/json
|
|||
|
|
|
|||
|
|
{
|
|||
|
|
"type": "chat",
|
|||
|
|
"content": "测试消息",
|
|||
|
|
"timestamp": 1234567890
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 获取消息数量
|
|||
|
|
```http
|
|||
|
|
GET http://localhost:8081/api/front/offline-messages/count/1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 获取消息列表
|
|||
|
|
```http
|
|||
|
|
GET http://localhost:8081/api/front/offline-messages/list/1?limit=50
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 获取所有消息
|
|||
|
|
```http
|
|||
|
|
GET http://localhost:8081/api/front/offline-messages/all/1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 删除指定数量
|
|||
|
|
```http
|
|||
|
|
DELETE http://localhost:8081/api/front/offline-messages/remove/1?count=5
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 清空所有消息
|
|||
|
|
```http
|
|||
|
|
DELETE http://localhost:8081/api/front/offline-messages/clear/1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 使用Redis CLI测试
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 查看用户1的离线消息
|
|||
|
|
redis-cli LRANGE offline:msg:1 0 -1
|
|||
|
|
|
|||
|
|
# 查看消息数量
|
|||
|
|
redis-cli LLEN offline:msg:1
|
|||
|
|
|
|||
|
|
# 查看过期时间
|
|||
|
|
redis-cli TTL offline:msg:1
|
|||
|
|
|
|||
|
|
# 手动添加消息
|
|||
|
|
redis-cli LPUSH offline:msg:1 '{"type":"chat","content":"测试"}'
|
|||
|
|
|
|||
|
|
# 手动删除消息
|
|||
|
|
redis-cli DEL offline:msg:1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 📈 性能优化
|
|||
|
|
|
|||
|
|
### 1. 批量操作
|
|||
|
|
- 使用Redis Pipeline批量获取多个用户的离线消息
|
|||
|
|
- 减少网络往返次数
|
|||
|
|
|
|||
|
|
### 2. 消息限制
|
|||
|
|
- 限制每个用户最多100条离线消息
|
|||
|
|
- 超过限制时自动删除最旧的消息
|
|||
|
|
- 避免内存占用过大
|
|||
|
|
|
|||
|
|
### 3. 过期策略
|
|||
|
|
- 设置7天过期时间
|
|||
|
|
- Redis自动清理过期数据
|
|||
|
|
- 减少手动清理的开销
|
|||
|
|
|
|||
|
|
### 4. 异步推送
|
|||
|
|
- 用户上线时异步推送离线消息
|
|||
|
|
- 不阻塞WebSocket连接建立
|
|||
|
|
- 提高用户体验
|
|||
|
|
|
|||
|
|
## 🔒 安全考虑
|
|||
|
|
|
|||
|
|
### 1. 权限验证
|
|||
|
|
- 只能获取自己的离线消息
|
|||
|
|
- 需要在Controller中添加Token验证
|
|||
|
|
- 防止越权访问
|
|||
|
|
|
|||
|
|
### 2. 消息加密
|
|||
|
|
- 敏感消息可以加密存储
|
|||
|
|
- 推送时解密
|
|||
|
|
- 保护用户隐私
|
|||
|
|
|
|||
|
|
### 3. 限流保护
|
|||
|
|
- 限制API调用频率
|
|||
|
|
- 防止恶意刷接口
|
|||
|
|
- 保护系统稳定性
|
|||
|
|
|
|||
|
|
## 🐛 常见问题
|
|||
|
|
|
|||
|
|
### 1. 离线消息没有推送?
|
|||
|
|
|
|||
|
|
**可能原因:**
|
|||
|
|
- 用户没有真正离线(还有其他设备在线)
|
|||
|
|
- WebSocket连接建立失败
|
|||
|
|
- Redis连接异常
|
|||
|
|
|
|||
|
|
**解决方法:**
|
|||
|
|
- 检查在线状态服务
|
|||
|
|
- 查看WebSocket连接日志
|
|||
|
|
- 检查Redis连接
|
|||
|
|
|
|||
|
|
### 2. 离线消息丢失?
|
|||
|
|
|
|||
|
|
**可能原因:**
|
|||
|
|
- Redis数据过期
|
|||
|
|
- 消息超过100条被删除
|
|||
|
|
- Redis重启导致数据丢失
|
|||
|
|
|
|||
|
|
**解决方法:**
|
|||
|
|
- 增加过期时间
|
|||
|
|
- 增加最大消息数量
|
|||
|
|
- 启用Redis持久化(AOF/RDB)
|
|||
|
|
|
|||
|
|
### 3. 离线消息重复推送?
|
|||
|
|
|
|||
|
|
**可能原因:**
|
|||
|
|
- 清除消息失败
|
|||
|
|
- 用户多次连接
|
|||
|
|
- 并发问题
|
|||
|
|
|
|||
|
|
**解决方法:**
|
|||
|
|
- 添加消息去重逻辑
|
|||
|
|
- 使用分布式锁
|
|||
|
|
- 检查清除逻辑
|
|||
|
|
|
|||
|
|
## 📝 后续优化建议
|
|||
|
|
|
|||
|
|
### 1. 消息持久化
|
|||
|
|
- 将重要消息同时保存到MySQL
|
|||
|
|
- Redis作为缓存,MySQL作为持久化存储
|
|||
|
|
- 提高数据可靠性
|
|||
|
|
|
|||
|
|
### 2. 消息优先级
|
|||
|
|
- 支持消息优先级
|
|||
|
|
- 重要消息优先推送
|
|||
|
|
- 普通消息延迟推送
|
|||
|
|
|
|||
|
|
### 3. 消息分类
|
|||
|
|
- 支持不同类型的离线消息
|
|||
|
|
- 文本消息、图片消息、系统通知等
|
|||
|
|
- 分别处理和展示
|
|||
|
|
|
|||
|
|
### 4. 消息统计
|
|||
|
|
- 统计离线消息的发送量
|
|||
|
|
- 分析用户活跃度
|
|||
|
|
- 优化推送策略
|
|||
|
|
|
|||
|
|
### 5. 消息推送优化
|
|||
|
|
- 支持增量推送
|
|||
|
|
- 避免一次性推送大量消息
|
|||
|
|
- 提高用户体验
|
|||
|
|
|
|||
|
|
## 📚 相关文档
|
|||
|
|
|
|||
|
|
- [直播IM系统开发指南.md](./直播IM系统开发指南.md)
|
|||
|
|
- [WebSocket增强功能实现说明.md](./WebSocket增强功能实现说明.md)
|
|||
|
|
- [功能验证清单.md](./功能验证清单.md)
|
|||
|
|
|
|||
|
|
## 🎉 总结
|
|||
|
|
|
|||
|
|
离线消息功能已经完整实现,包括:
|
|||
|
|
|
|||
|
|
✅ 消息保存和推送
|
|||
|
|
✅ HTTP接口管理
|
|||
|
|
✅ WebSocket集成
|
|||
|
|
✅ 定时清理任务
|
|||
|
|
✅ 测试页面
|
|||
|
|
✅ 完整文档
|
|||
|
|
|
|||
|
|
可以开始测试和使用了!
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**文档版本:** v1.0
|
|||
|
|
**最后更新:** 2024-12-25
|
|||
|
|
**作者:** Kiro AI Assistant
|