miniprogramme/utils/websocket-manager-v2.js
2025-09-12 16:08:17 +08:00

761 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const config = require('../config/config.js');
/**
* 微信小程序专用WebSocket管理器 V2
* 针对小程序环境优化,解决连接问题
*/
class WebSocketManagerV2 {
constructor() {
this.ws = null;
this.isConnected = false;
this.isConnecting = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectInterval = 2000; // 2秒
this.heartbeatInterval = null;
this.heartbeatTimeout = 30000; // 30秒心跳
this.connectionTimeout = null;
// 认证信息
this.token = null;
this.deviceId = null;
// 事件处理器
this.messageHandlers = new Map();
this.eventHandlers = new Map();
// 消息队列
this.messageQueue = [];
this.messageIdCounter = 0;
// 初始化设备ID
this.initDeviceId();
// 绑定方法上下文
this.onOpen = this.onOpen.bind(this);
this.onMessage = this.onMessage.bind(this);
this.onError = this.onError.bind(this);
this.onClose = this.onClose.bind(this);
}
// 初始化设备ID
initDeviceId() {
try {
let deviceId = wx.getStorageSync('device_id');
if (!deviceId) {
deviceId = 'mp_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
wx.setStorageSync('device_id', deviceId);
}
this.deviceId = deviceId;
console.log('🔧 设备ID初始化:', this.deviceId);
} catch (error) {
console.error('❌ 设备ID初始化失败:', error);
this.deviceId = 'mp_unknown_' + Date.now();
}
}
// 设置认证token
setToken(token) {
this.token = token;
console.log('🔑 Token已设置:', token ? '成功' : '失败');
}
// 获取认证token
getToken() {
if (this.token) {
return this.token;
}
try {
// 方式1从app.globalData获取
const app = getApp();
if (app?.globalData?.userInfo?.token) {
this.token = app.globalData.userInfo.token;
console.log('📱 从app.globalData获取token成功');
return this.token;
}
// 方式2从本地存储获取userInfo
const userInfo = wx.getStorageSync('userInfo');
if (userInfo?.token) {
this.token = userInfo.token;
console.log('💾 从userInfo获取token成功');
return this.token;
}
// 方式3直接从本地存储获取token
const directToken = wx.getStorageSync('token');
if (directToken) {
this.token = directToken;
console.log('💾 从storage直接获取token成功');
return this.token;
}
console.warn('⚠️ 未找到有效的token');
return null;
} catch (error) {
console.error('❌ 获取token失败:', error);
return null;
}
}
// 连接WebSocket
async connect() {
if (this.isConnected) {
console.log('✅ WebSocket已连接');
return Promise.resolve();
}
if (this.isConnecting) {
console.log('⚠️ WebSocket正在连接中等待完成...');
return this.waitForConnection();
}
return new Promise((resolve, reject) => {
console.log('🚀 开始连接WebSocket...');
this.isConnecting = true;
// 获取token
const token = this.getToken();
if (!token) {
console.error('❌ 缺少认证token无法连接WebSocket');
this.isConnecting = false;
reject(new Error('缺少认证token'));
return;
}
// 构建WebSocket URL
const wsUrl = config.websocket?.url || 'wss://api.faxianwo.me/api/v1/ws';
const fullUrl = `${wsUrl}?device_id=${encodeURIComponent(this.deviceId)}`;
console.log('📡 连接信息:', {
url: fullUrl,
deviceId: this.deviceId,
hasToken: !!token,
tokenLength: token.length
});
try {
// 小程序WebSocket连接配置
const connectOptions = {
url: fullUrl,
header: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
timeout: 15000
};
this.ws = wx.connectSocket(connectOptions);
console.log('📡 WebSocket连接对象已创建');
// 设置连接超时
this.connectionTimeout = setTimeout(() => {
if (this.isConnecting && !this.isConnected) {
console.error('❌ WebSocket连接超时');
this.cleanup();
reject(new Error('连接超时'));
}
}, 20000);
// 事件监听
this.ws.onOpen((res) => {
console.log('🎉 WebSocket连接成功');
this.onOpen(res);
resolve();
});
this.ws.onMessage((res) => {
this.onMessage(res);
});
this.ws.onError((error) => {
console.error('💥 WebSocket连接错误:', error);
this.onError(error);
if (this.isConnecting) {
reject(error);
}
});
this.ws.onClose((res) => {
console.log('🔌 WebSocket连接关闭');
this.onClose(res);
if (this.isConnecting) {
reject(new Error('连接被关闭'));
}
});
} catch (error) {
console.error('❌ WebSocket连接创建失败:', error);
this.cleanup();
// 检查域名配置问题
if (error.errMsg?.includes('url not in domain list')) {
console.error('💡 请在微信公众平台配置WebSocket域名: wss://api.faxianwo.me');
}
reject(error);
}
});
}
// 等待连接完成
waitForConnection() {
return new Promise((resolve, reject) => {
const checkConnection = () => {
if (this.isConnected) {
resolve();
} else if (!this.isConnecting) {
reject(new Error('连接失败'));
} else {
setTimeout(checkConnection, 100);
}
};
checkConnection();
});
}
// 清理连接状态
cleanup() {
this.isConnecting = false;
if (this.connectionTimeout) {
clearTimeout(this.connectionTimeout);
this.connectionTimeout = null;
}
}
// 连接成功处理
onOpen(res) {
console.log('✅ WebSocket连接建立成功');
console.log('📊 连接详情:', {
deviceId: this.deviceId,
hasToken: !!this.token,
reconnectAttempts: this.reconnectAttempts,
timestamp: new Date().toLocaleTimeString()
});
this.isConnected = true;
this.isConnecting = false;
this.reconnectAttempts = 0;
this.cleanup();
this.startHeartbeat();
this.processMessageQueue();
this.triggerEvent('connected', res);
}
// 消息处理
onMessage(event) {
try {
const message = JSON.parse(event.data);
console.log('📨 收到消息:', message.type || 'unknown');
// 心跳响应
if (message.type === 'pong' || message.type === 'heartbeat_response') {
console.log('💓 心跳响应');
return;
}
// 🔥 根据消息类型分发到对应的事件处理器
switch (message.type) {
case 'new_message':
this.triggerEvent('new_message', message);
break;
case 'message_status':
this.triggerEvent('message_status', message);
break;
case 'message_sent':
// 标准响应:消息发送成功(包括撤回成功)
this.triggerEvent('message_sent', message);
break;
case 'message_recalled':
// 收到其他用户撤回消息的通知
this.triggerEvent('message_recalled', message);
break;
case 'error':
// 错误响应(包括撤回失败)
this.triggerEvent('error', message);
break;
case 'unread_count_update':
this.triggerEvent('unread_count_update', message);
break;
// 🔥 在线状态相关presence
case 'user_online':
this.triggerEvent('user_online', message);
this.triggerEvent('message', message); // 兼容通用监听
break;
case 'user_offline':
this.triggerEvent('user_offline', message);
this.triggerEvent('message', message); // 兼容通用监听
break;
case 'presence_update':
this.triggerEvent('presence_update', message);
this.triggerEvent('message', message); // 兼容通用监听
break;
case 'friend_request':
this.triggerEvent('friend_request', message);
break;
case 'notification':
this.triggerEvent('notification', message);
break;
case 'chat_message':
this.triggerEvent('chat_message', message);
break;
default:
// 尝试查找特定类型的处理器
const handler = this.messageHandlers.get(message.type);
if (handler) {
handler(message);
} else {
// 触发通用消息事件
this.triggerEvent('message', message);
}
}
} catch (error) {
console.error('❌ 消息解析失败:', error);
}
}
// 错误处理
onError(error) {
console.error('❌ WebSocket错误:', error);
this.isConnected = false;
this.cleanup();
this.triggerEvent('error', error);
this.scheduleReconnect();
}
// 连接关闭处理
onClose(event) {
console.log('🔌 WebSocket连接关闭:', event.code);
this.isConnected = false;
this.cleanup();
this.stopHeartbeat();
this.triggerEvent('disconnected', event);
// 非正常关闭时重连
if (event.code !== 1000) {
this.scheduleReconnect();
}
}
// 发送消息
send(message) {
if (!this.isConnected) {
console.warn('⚠️ WebSocket未连接消息加入队列');
this.messageQueue.push(message);
this.connect().catch(console.error);
return false;
}
try {
const messageStr = typeof message === 'string' ? message : JSON.stringify(message);
this.ws.send({ data: messageStr });
console.log('📤 消息发送成功');
return true;
} catch (error) {
console.error('❌ 消息发送失败:', error);
return false;
}
}
// 处理消息队列
processMessageQueue() {
while (this.messageQueue.length > 0 && this.isConnected) {
const message = this.messageQueue.shift();
this.send(message);
}
}
// 开始心跳 - 使用WebSocket原生ping/pong
startHeartbeat() {
this.stopHeartbeat();
this.heartbeatInterval = setInterval(() => {
if (this.isConnected && this.ws) {
try {
// 使用微信小程序的ping方法如果支持
if (typeof this.ws.ping === 'function') {
this.ws.ping();
console.log('💓 发送WebSocket原生ping');
} else {
// 降级:发送应用层心跳(使用正确的格式)
const heartbeatMessage = {
type: 'heartbeat',
id: `heartbeat_${Date.now()}`,
data: {
timestamp: Date.now()
}
};
this.send(heartbeatMessage);
console.log('💓 发送应用层心跳');
}
} catch (error) {
console.error('❌ 心跳发送失败:', error);
}
}
}, this.heartbeatTimeout);
}
// 停止心跳
stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
// 计划重连
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('❌ 达到最大重连次数,停止重连');
return;
}
const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts);
console.log(`🔄 ${delay}ms后尝试第${this.reconnectAttempts + 1}次重连`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect().catch(console.error);
}, delay);
}
// 断开连接
disconnect() {
console.log('🔌 主动断开WebSocket连接');
this.stopHeartbeat();
this.cleanup();
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.isConnected = false;
this.reconnectAttempts = this.maxReconnectAttempts; // 阻止重连
}
// 注册消息处理器
onMessage(type, handler) {
this.messageHandlers.set(type, handler);
}
// 发送聊天消息 - 符合API文档规范
sendChatMessage(receiverId, content, msgType = 0, chatType = 0, options = {}) {
// 生成客户端消息ID
const clientMsgId = this.generateMessageId();
// 🔥 消息类型映射 - 修正为与API文档一致
const msgTypeMap = {
'text': 0, // 文字消息
'image': 1, // 图片消息
'voice': 2, // 语音消息
'video': 3, // 视频消息
'file': 4, // 文件消息
'emoji': 6, // 表情消息
'location': 5 // 位置消息
};
// 🔥 详细记录消息类型转换过程
console.log('🔍 消息类型转换详情:', {
originalMsgType: msgType,
originalType: typeof msgType,
msgTypeMap: msgTypeMap
});
// 如果msgType是字符串转换为数字
if (typeof msgType === 'string') {
const convertedMsgType = msgTypeMap[msgType] || 0;
console.log('🔄 字符串转数字:', msgType, '->', convertedMsgType);
msgType = convertedMsgType;
}
console.log('✅ 最终msgType:', msgType);
const message = {
type: 'send_message',
id: clientMsgId,
data: {
receiverId: receiverId,
chatType: chatType,
msgType: msgType,
content: content,
atUsers: options.atUsers || [],
replyTo: options.replyTo || '',
extra: options.extra || ''
}
};
console.log('📤 发送聊天消息:', {
type: 'send_message',
id: clientMsgId,
receiverId,
chatType,
msgType,
contentLength: content ? content.length : 0,
contentPreview: content ? (content.substring(0, 50) + (content.length > 50 ? '...' : '')) : 'null'
});
return this.send(message);
}
// 生成消息ID
generateMessageId() {
this.messageIdCounter++;
return `client_msg_${Date.now()}_${this.messageIdCounter}`;
}
// 发送图片消息
sendImageMessage(receiverId, imageUrl, chatType = 0, options = {}) {
return this.sendChatMessage(receiverId, imageUrl, 1, chatType, options);
}
// 发送语音消息
sendVoiceMessage(receiverId, voiceUrl, duration, chatType = 0, options = {}) {
const content = JSON.stringify({
url: voiceUrl,
duration: duration
});
return this.sendChatMessage(receiverId, content, 2, chatType, options);
}
// 发送视频消息
sendVideoMessage(receiverId, videoUrl, duration, thumbnail, chatType = 0, options = {}) {
const content = JSON.stringify({
url: videoUrl,
duration: duration,
thumbnail: thumbnail
});
return this.sendChatMessage(receiverId, content, 3, chatType, options);
}
// 发送位置消息
sendLocationMessage(receiverId, latitude, longitude, address, locationName, chatType = 0, options = {}) {
const message = {
type: 'send_location',
id: this.generateMessageId(),
data: {
receiverId: receiverId,
chatType: chatType,
latitude: latitude,
longitude: longitude,
address: address,
locationName: locationName
}
};
return this.send(message);
}
// 撤回消息
recallMessage(messageId) {
const message = {
type: 'recall_message',
id: this.generateMessageId(),
data: {
messageId: messageId
}
};
console.log('📤 发送撤回消息请求:', message);
return this.send(message);
}
// 标记消息已读
markMessageRead(messageIds) {
const message = {
type: 'mark_read',
id: this.generateMessageId(),
data: {
messageIds: Array.isArray(messageIds) ? messageIds : [messageIds]
}
};
return this.send(message);
}
// 注册事件处理器
on(event, handler) {
if (!this.eventHandlers.has(event)) {
this.eventHandlers.set(event, []);
}
this.eventHandlers.get(event).push(handler);
}
// 移除事件处理器
off(event, handler) {
if (!this.eventHandlers.has(event)) {
return;
}
const handlers = this.eventHandlers.get(event);
if (handler) {
// 移除特定的处理器
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
} else {
// 移除所有处理器
this.eventHandlers.set(event, []);
}
// 如果没有处理器了,删除整个事件
if (handlers.length === 0) {
this.eventHandlers.delete(event);
}
}
// 触发事件
triggerEvent(event, data) {
const handlers = this.eventHandlers.get(event);
if (handlers) {
handlers.forEach(handler => {
try {
handler(data);
} catch (error) {
console.error(`❌ 事件处理器错误 [${event}]:`, error);
}
});
}
}
// 获取连接状态
getStatus() {
return {
isConnected: this.isConnected,
isConnecting: this.isConnecting,
reconnectAttempts: this.reconnectAttempts,
hasToken: !!this.token,
deviceId: this.deviceId
};
}
// 兼容性方法 - 为了向后兼容
getConnectionStatus() {
return {
connected: this.isConnected,
connecting: this.isConnecting
};
}
// ===== 聊天功能相关方法 =====
// 发送输入状态
sendTypingStatus(conversationId, isTyping) {
try {
if (!this.isConnected) {
console.warn('⚠️ WebSocket未连接无法发送输入状态');
return false;
}
const message = {
type: 'typing_status',
id: `typing_${Date.now()}`,
data: {
conversationId: conversationId,
isTyping: isTyping,
timestamp: Date.now()
}
};
console.log('📝 发送输入状态:', {
conversationId,
isTyping,
timestamp: Date.now()
});
return this.send(message);
} catch (error) {
console.error('❌ 发送输入状态失败:', error);
return false;
}
}
// 发送已读回执
sendReadReceipt(messageId) {
try {
if (!this.isConnected) {
console.warn('⚠️ WebSocket未连接无法发送已读回执');
return false;
}
const message = {
type: 'mark_read',
id: `read_${Date.now()}`,
data: {
messageId: messageId,
timestamp: Date.now()
}
};
console.log('✅ 发送已读回执:', {
messageId,
timestamp: Date.now()
});
return this.send(message);
} catch (error) {
console.error('❌ 发送已读回执失败:', error);
return false;
}
}
// 聊天消息处理器注册
onChatMessage(handler) {
this.on('new_message', handler);
this.on('chat_message', handler);
}
// 未读数更新处理器注册
onUnreadCountUpdate(handler) {
this.on('unread_count_update', handler);
this.on('unread_update', handler);
}
// 通知处理器注册
onNotification(handler) {
this.on('notification', handler);
}
// 好友请求处理器注册
onFriendRequest(handler) {
this.on('friend_request', handler);
}
// 发送聊天消息(兼容旧版本接口)- 重定向到标准方法
sendChatMessageLegacy(targetId, content, messageType = 'text', extra = {}) {
// 转换消息类型
const msgTypeMap = {
'text': 1,
'image': 2,
'voice': 3,
'video': 4,
'file': 5,
'emoji': 6,
'location': 7
};
const msgType = msgTypeMap[messageType] || 1;
const chatType = extra.chatType || 0;
return this.sendChatMessage(targetId, content, msgType, chatType, extra);
}
// 生成消息ID
generateMessageId() {
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// ===== 兼容性别名方法 =====
// 兼容旧版本的 sendMessage 方法
sendMessage(message) {
return this.send(message);
}
}
// 创建全局实例
const wsManager = new WebSocketManagerV2();
module.exports = wsManager;