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
|