301 lines
8.9 KiB
JavaScript
301 lines
8.9 KiB
JavaScript
|
|
/**
|
|||
|
|
* 聊天消息处理器
|
|||
|
|
* 专门处理WebSocket接收到的聊天相关消息
|
|||
|
|
*/
|
|||
|
|
class ChatMessageHandler {
|
|||
|
|
constructor() {
|
|||
|
|
this.messageListeners = new Map();
|
|||
|
|
this.statusListeners = new Map();
|
|||
|
|
this.conversationListeners = new Map();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理WebSocket消息
|
|||
|
|
handleMessage(message) {
|
|||
|
|
console.log('🔄 聊天消息处理器收到消息:', message.type);
|
|||
|
|
|
|||
|
|
switch (message.type) {
|
|||
|
|
case 'new_message':
|
|||
|
|
this.handleNewMessage(message.data);
|
|||
|
|
break;
|
|||
|
|
case 'message_status_update':
|
|||
|
|
this.handleMessageStatusUpdate(message.data);
|
|||
|
|
break;
|
|||
|
|
case 'message_recalled':
|
|||
|
|
this.handleMessageRecalled(message.data);
|
|||
|
|
break;
|
|||
|
|
case 'conversation_update':
|
|||
|
|
this.handleConversationUpdate(message.data);
|
|||
|
|
break;
|
|||
|
|
case 'typing_status':
|
|||
|
|
this.handleTypingStatus(message.data);
|
|||
|
|
break;
|
|||
|
|
case 'read_receipt':
|
|||
|
|
this.handleReadReceipt(message.data);
|
|||
|
|
break;
|
|||
|
|
default:
|
|||
|
|
console.log('未处理的聊天消息类型:', message.type);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理新消息
|
|||
|
|
handleNewMessage(messageData) {
|
|||
|
|
console.log('📨 收到新消息:', messageData);
|
|||
|
|
|
|||
|
|
// 格式化消息数据
|
|||
|
|
const formattedMessage = this.formatMessage(messageData);
|
|||
|
|
|
|||
|
|
// 通知所有监听器
|
|||
|
|
this.notifyMessageListeners('new_message', formattedMessage);
|
|||
|
|
|
|||
|
|
// 更新会话信息
|
|||
|
|
this.updateConversationLastMessage(formattedMessage);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理消息状态更新
|
|||
|
|
handleMessageStatusUpdate(statusData) {
|
|||
|
|
console.log('📊 消息状态更新:', statusData);
|
|||
|
|
|
|||
|
|
this.notifyStatusListeners('status_update', statusData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理消息撤回
|
|||
|
|
handleMessageRecalled(recallData) {
|
|||
|
|
console.log('↩️ 消息被撤回:', recallData);
|
|||
|
|
|
|||
|
|
this.notifyMessageListeners('message_recalled', recallData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理会话更新
|
|||
|
|
handleConversationUpdate(conversationData) {
|
|||
|
|
console.log('💬 会话更新:', conversationData);
|
|||
|
|
|
|||
|
|
this.notifyConversationListeners('conversation_update', conversationData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理输入状态
|
|||
|
|
handleTypingStatus(typingData) {
|
|||
|
|
console.log('⌨️ 输入状态:', typingData);
|
|||
|
|
|
|||
|
|
this.notifyMessageListeners('typing_status', typingData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理已读回执
|
|||
|
|
handleReadReceipt(readData) {
|
|||
|
|
console.log('✅ 已读回执:', readData);
|
|||
|
|
|
|||
|
|
this.notifyStatusListeners('read_receipt', readData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化消息数据
|
|||
|
|
formatMessage(messageData) {
|
|||
|
|
return {
|
|||
|
|
messageId: messageData.messageId,
|
|||
|
|
senderId: messageData.senderId,
|
|||
|
|
receiverId: messageData.receiverId,
|
|||
|
|
conversationId: messageData.conversationId || this.generateConversationId(messageData.senderId, messageData.receiverId),
|
|||
|
|
chatType: messageData.chatType || 0,
|
|||
|
|
msgType: messageData.msgType || 1,
|
|||
|
|
content: messageData.content || '',
|
|||
|
|
status: messageData.status || 1,
|
|||
|
|
sendTime: messageData.sendTime || new Date().toISOString(),
|
|||
|
|
senderName: messageData.senderName || '',
|
|||
|
|
senderAvatar: messageData.senderAvatar || '',
|
|||
|
|
replyTo: messageData.replyTo || '',
|
|||
|
|
atUsers: messageData.atUsers || [],
|
|||
|
|
extra: messageData.extra || '',
|
|||
|
|
// 客户端扩展字段
|
|||
|
|
isOwn: false, // 需要在使用时设置
|
|||
|
|
displayTime: this.formatDisplayTime(messageData.sendTime),
|
|||
|
|
contentType: this.getContentType(messageData.msgType),
|
|||
|
|
parsedContent: this.parseContent(messageData.content, messageData.msgType)
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 生成会话ID
|
|||
|
|
generateConversationId(senderId, receiverId) {
|
|||
|
|
// 单聊会话ID生成规则:较小的ID在前
|
|||
|
|
const ids = [senderId, receiverId].sort();
|
|||
|
|
return `conv_${ids[0]}_${ids[1]}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化显示时间
|
|||
|
|
formatDisplayTime(timeString) {
|
|||
|
|
if (!timeString) return '';
|
|||
|
|
|
|||
|
|
const messageTime = new Date(timeString);
|
|||
|
|
const now = new Date();
|
|||
|
|
const diffMs = now - messageTime;
|
|||
|
|
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
|||
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|||
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|||
|
|
|
|||
|
|
if (diffMinutes < 1) {
|
|||
|
|
return '刚刚';
|
|||
|
|
} else if (diffMinutes < 60) {
|
|||
|
|
return `${diffMinutes}分钟前`;
|
|||
|
|
} else if (diffHours < 24) {
|
|||
|
|
return messageTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|||
|
|
} else if (diffDays < 7) {
|
|||
|
|
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|||
|
|
return weekdays[messageTime.getDay()];
|
|||
|
|
} else {
|
|||
|
|
return messageTime.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取内容类型
|
|||
|
|
getContentType(msgType) {
|
|||
|
|
const typeMap = {
|
|||
|
|
1: 'text',
|
|||
|
|
2: 'image',
|
|||
|
|
3: 'voice',
|
|||
|
|
4: 'video',
|
|||
|
|
5: 'file',
|
|||
|
|
6: 'emoji',
|
|||
|
|
7: 'location',
|
|||
|
|
8: 'system',
|
|||
|
|
9: 'recall',
|
|||
|
|
10: 'redpacket'
|
|||
|
|
};
|
|||
|
|
return typeMap[msgType] || 'text';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 解析消息内容
|
|||
|
|
parseContent(content, msgType) {
|
|||
|
|
try {
|
|||
|
|
switch (msgType) {
|
|||
|
|
case 2: // 图片
|
|||
|
|
return { url: content, type: 'image' };
|
|||
|
|
case 3: // 语音
|
|||
|
|
const voiceData = JSON.parse(content);
|
|||
|
|
return { url: voiceData.url, duration: voiceData.duration, type: 'voice' };
|
|||
|
|
case 4: // 视频
|
|||
|
|
const videoData = JSON.parse(content);
|
|||
|
|
return { url: videoData.url, duration: videoData.duration, thumbnail: videoData.thumbnail, type: 'video' };
|
|||
|
|
case 5: // 文件
|
|||
|
|
const fileData = JSON.parse(content);
|
|||
|
|
return { url: fileData.url, name: fileData.name, size: fileData.size, type: 'file' };
|
|||
|
|
case 7: // 位置
|
|||
|
|
const locationData = JSON.parse(content);
|
|||
|
|
return { latitude: locationData.latitude, longitude: locationData.longitude, address: locationData.address, type: 'location' };
|
|||
|
|
default:
|
|||
|
|
return { text: content, type: 'text' };
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.warn('解析消息内容失败:', error);
|
|||
|
|
return { text: content, type: 'text' };
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 更新会话最后消息
|
|||
|
|
updateConversationLastMessage(message) {
|
|||
|
|
const conversationUpdate = {
|
|||
|
|
conversationId: message.conversationId,
|
|||
|
|
lastMessage: {
|
|||
|
|
content: this.getDisplayContent(message),
|
|||
|
|
sendTime: message.sendTime,
|
|||
|
|
senderId: message.senderId,
|
|||
|
|
senderName: message.senderName
|
|||
|
|
},
|
|||
|
|
unreadCount: message.isOwn ? 0 : 1 // 如果是自己发的消息,未读数为0
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
this.notifyConversationListeners('last_message_update', conversationUpdate);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取显示内容
|
|||
|
|
getDisplayContent(message) {
|
|||
|
|
switch (message.msgType) {
|
|||
|
|
case 1: return message.content;
|
|||
|
|
case 2: return '[图片]';
|
|||
|
|
case 3: return '[语音]';
|
|||
|
|
case 4: return '[视频]';
|
|||
|
|
case 5: return '[文件]';
|
|||
|
|
case 6: return '[表情]';
|
|||
|
|
case 7: return '[位置]';
|
|||
|
|
case 8: return message.content;
|
|||
|
|
case 9: return '撤回了一条消息';
|
|||
|
|
case 10: return '[红包]';
|
|||
|
|
default: return message.content;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 注册消息监听器
|
|||
|
|
onMessage(event, listener) {
|
|||
|
|
if (!this.messageListeners.has(event)) {
|
|||
|
|
this.messageListeners.set(event, []);
|
|||
|
|
}
|
|||
|
|
this.messageListeners.get(event).push(listener);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 注册状态监听器
|
|||
|
|
onStatus(event, listener) {
|
|||
|
|
if (!this.statusListeners.has(event)) {
|
|||
|
|
this.statusListeners.set(event, []);
|
|||
|
|
}
|
|||
|
|
this.statusListeners.get(event).push(listener);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 注册会话监听器
|
|||
|
|
onConversation(event, listener) {
|
|||
|
|
if (!this.conversationListeners.has(event)) {
|
|||
|
|
this.conversationListeners.set(event, []);
|
|||
|
|
}
|
|||
|
|
this.conversationListeners.get(event).push(listener);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 通知消息监听器
|
|||
|
|
notifyMessageListeners(event, data) {
|
|||
|
|
const listeners = this.messageListeners.get(event);
|
|||
|
|
if (listeners) {
|
|||
|
|
listeners.forEach(listener => {
|
|||
|
|
try {
|
|||
|
|
listener(data);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`消息监听器错误 [${event}]:`, error);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 通知状态监听器
|
|||
|
|
notifyStatusListeners(event, data) {
|
|||
|
|
const listeners = this.statusListeners.get(event);
|
|||
|
|
if (listeners) {
|
|||
|
|
listeners.forEach(listener => {
|
|||
|
|
try {
|
|||
|
|
listener(data);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`状态监听器错误 [${event}]:`, error);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 通知会话监听器
|
|||
|
|
notifyConversationListeners(event, data) {
|
|||
|
|
const listeners = this.conversationListeners.get(event);
|
|||
|
|
if (listeners) {
|
|||
|
|
listeners.forEach(listener => {
|
|||
|
|
try {
|
|||
|
|
listener(data);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`会话监听器错误 [${event}]:`, error);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 清理监听器
|
|||
|
|
removeAllListeners() {
|
|||
|
|
this.messageListeners.clear();
|
|||
|
|
this.statusListeners.clear();
|
|||
|
|
this.conversationListeners.clear();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建全局实例
|
|||
|
|
const chatMessageHandler = new ChatMessageHandler();
|
|||
|
|
|
|||
|
|
module.exports = chatMessageHandler;
|