762 lines
20 KiB
JavaScript
762 lines
20 KiB
JavaScript
|
|
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;
|