2026-01-09 13:58:09 +08:00
|
|
|
|
import {checkDev} from "@/utils/utils";
|
|
|
|
|
|
|
2025-07-19 16:04:43 +08:00
|
|
|
|
export class WebSocketManager {
|
|
|
|
|
|
constructor(url) {
|
|
|
|
|
|
this.url = url;
|
|
|
|
|
|
this.socket = null;
|
|
|
|
|
|
this.isConnected = false;
|
2025-12-30 10:04:30 +08:00
|
|
|
|
this.isAuthenticated = false; // 是否已认证
|
2025-07-19 16:04:43 +08:00
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
|
|
this.maxReconnectAttempts = 5;
|
|
|
|
|
|
this.reconnectDelay = 1000;
|
|
|
|
|
|
this.messageHandlers = [];
|
|
|
|
|
|
this.pingInterval = null;
|
2025-12-30 10:04:30 +08:00
|
|
|
|
this.cachedToken = null; // 缓存的token
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前用户的token
|
|
|
|
|
|
* @returns {string|null}
|
|
|
|
|
|
*/
|
|
|
|
|
|
getToken() {
|
2026-01-12 12:36:50 +08:00
|
|
|
|
// 从本地存储获取token(不使用缓存,确保获取最新token)
|
2025-12-30 10:04:30 +08:00
|
|
|
|
let token = uni.getStorageSync('token');
|
|
|
|
|
|
if (token) {
|
|
|
|
|
|
// 移除Bearer前缀(如果有)
|
|
|
|
|
|
token = token.replace('Bearer ', '');
|
|
|
|
|
|
this.cachedToken = token;
|
|
|
|
|
|
}
|
|
|
|
|
|
return token || null;
|
2025-07-19 16:04:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-12 12:36:50 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 清除缓存的token(登录后需要调用,以获取最新token)
|
|
|
|
|
|
*/
|
|
|
|
|
|
clearCachedToken() {
|
|
|
|
|
|
this.cachedToken = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 重新绑定用户(登录后调用)
|
2026-02-07 11:12:18 +08:00
|
|
|
|
* 会先清理旧连接,再绑定新连接
|
2026-01-12 12:36:50 +08:00
|
|
|
|
*/
|
|
|
|
|
|
rebindUser() {
|
|
|
|
|
|
const userId = uni.getStorageSync('user_id');
|
|
|
|
|
|
if (!userId) {
|
|
|
|
|
|
console.warn('重新绑定用户失败:user_id不存在');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清除缓存的token,确保获取最新token
|
|
|
|
|
|
this.clearCachedToken();
|
|
|
|
|
|
|
|
|
|
|
|
if (this.isConnected) {
|
2026-02-07 11:12:18 +08:00
|
|
|
|
// 如果已连接,直接bind(Go后端会自动清理旧连接)
|
|
|
|
|
|
// 注意:不在这里调用unbind,因为unbind会关闭当前连接
|
|
|
|
|
|
return this.bindUser(userId, true);
|
2026-01-12 12:36:50 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 如果未连接,先连接再绑定
|
|
|
|
|
|
this.connect();
|
2026-02-07 11:12:18 +08:00
|
|
|
|
// 连接成功后会在 onOpen 中自动绑定(会自动清理旧连接)
|
2026-01-12 12:36:50 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-19 16:04:43 +08:00
|
|
|
|
connect() {
|
|
|
|
|
|
if (this.isConnected) return;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('正在连接WebSocket...');
|
|
|
|
|
|
this.socket = uni.connectSocket({
|
|
|
|
|
|
url: this.url,
|
|
|
|
|
|
success: () => {
|
|
|
|
|
|
console.log('WebSocket连接成功');
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error('WebSocket连接失败:', err);
|
|
|
|
|
|
this.handleReconnect();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.socket.onOpen(() => {
|
|
|
|
|
|
console.log('WebSocket连接已打开');
|
|
|
|
|
|
this.isConnected = true;
|
|
|
|
|
|
this.reconnectAttempts = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 开始心跳检测
|
|
|
|
|
|
this.startHeartbeat();
|
|
|
|
|
|
|
2026-01-12 12:36:50 +08:00
|
|
|
|
// 绑定当前用户(使用正确的key:user_id)
|
|
|
|
|
|
const userId = uni.getStorageSync('user_id');
|
2025-07-19 16:04:43 +08:00
|
|
|
|
if (userId) {
|
2026-02-07 11:12:18 +08:00
|
|
|
|
// 连接打开后立即绑定,Go后端会自动清理旧连接
|
|
|
|
|
|
this.bindUser(userId, true);
|
2025-07-19 16:04:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.socket.onMessage((res) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(res.data);
|
|
|
|
|
|
console.log('收到消息:', data);
|
|
|
|
|
|
this.handleMessage(data);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('解析消息失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.socket.onClose((res) => {
|
|
|
|
|
|
console.log('WebSocket连接关闭:', res);
|
|
|
|
|
|
this.isConnected = false;
|
|
|
|
|
|
this.stopHeartbeat();
|
|
|
|
|
|
this.handleReconnect();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
this.socket.onError((err) => {
|
|
|
|
|
|
console.error('WebSocket错误:', err);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-07 11:12:18 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 解绑用户(清理旧连接)
|
|
|
|
|
|
* 注意:此方法会关闭当前连接,只应在需要完全断开时使用
|
|
|
|
|
|
* @param {string} userId 用户ID(格式:user-xxx 或 数字)
|
|
|
|
|
|
* @returns {boolean} 是否发送成功
|
|
|
|
|
|
*/
|
|
|
|
|
|
unbindUser(userId) {
|
|
|
|
|
|
const token = this.getToken();
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
|
console.warn('解绑用户失败:token不存在,跳过解绑');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 确保userId格式正确(如果是数字,添加user-前缀)
|
|
|
|
|
|
const normalizedUserId = userId.toString().startsWith('user-') ? userId : `user-${userId}`;
|
|
|
|
|
|
console.log('发送unbind请求,清理旧连接:', normalizedUserId);
|
|
|
|
|
|
return this.send({
|
|
|
|
|
|
request_type: 'unbind',
|
|
|
|
|
|
user_type: 'user',
|
|
|
|
|
|
sender_user_id: normalizedUserId,
|
|
|
|
|
|
token: token
|
|
|
|
|
|
}, false); // unbind不需要再次添加token
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 10:04:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 绑定用户(携带token进行认证)
|
2026-02-07 11:12:18 +08:00
|
|
|
|
* 在绑定前会自动清理旧连接(通过Go后端的clean_old_connections功能)
|
|
|
|
|
|
* @param {string} userId 用户ID(可以是数字或user-xxx格式)
|
|
|
|
|
|
* @param {boolean} cleanOldConnections 是否清理旧连接(默认true)
|
2025-12-30 10:04:30 +08:00
|
|
|
|
*/
|
2026-02-07 11:12:18 +08:00
|
|
|
|
bindUser(userId, cleanOldConnections = true) {
|
2025-12-30 10:04:30 +08:00
|
|
|
|
const token = this.getToken();
|
|
|
|
|
|
if (!token) {
|
|
|
|
|
|
console.error('绑定用户失败:token不存在');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2026-02-07 11:12:18 +08:00
|
|
|
|
|
|
|
|
|
|
// 确保userId格式正确(统一为user-xxx格式)
|
|
|
|
|
|
// 如果userId已经是user-xxx格式,保持不变;如果是数字,添加user-前缀
|
|
|
|
|
|
const normalizedUserId = userId.toString().startsWith('user-') ? userId : `user-${userId}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 注意:不在bind前发送unbind,因为unbind会关闭当前连接
|
|
|
|
|
|
// 清理旧连接的工作交给Go后端的clean_old_connections功能处理
|
|
|
|
|
|
|
|
|
|
|
|
// 发送bind请求,添加clean_old_connections标志告诉Go后端清理旧连接
|
|
|
|
|
|
console.log('发送bind请求,绑定新连接:', normalizedUserId, 'cleanOldConnections:', cleanOldConnections);
|
2025-12-30 10:04:30 +08:00
|
|
|
|
return this.send({
|
|
|
|
|
|
request_type: 'bind',
|
|
|
|
|
|
user_type: 'user',
|
2026-02-07 11:12:18 +08:00
|
|
|
|
sender_user_id: normalizedUserId,
|
|
|
|
|
|
token: token,
|
|
|
|
|
|
clean_old_connections: cleanOldConnections // 告诉Go后端清理旧连接
|
2025-07-19 16:04:43 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 10:04:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 发送消息(自动携带token)
|
|
|
|
|
|
* @param {Object} message 消息对象
|
|
|
|
|
|
* @param {boolean} requireAuth 是否需要认证(默认true)
|
|
|
|
|
|
* @returns {boolean} 是否发送成功
|
|
|
|
|
|
*/
|
|
|
|
|
|
send(message, requireAuth = true) {
|
2025-07-19 16:04:43 +08:00
|
|
|
|
if (!this.isConnected) {
|
|
|
|
|
|
console.error('WebSocket未连接');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
2025-12-30 10:04:30 +08:00
|
|
|
|
// 如果需要认证且消息中没有token,自动添加token
|
|
|
|
|
|
if (requireAuth && !message.token && message.request_type !== 'ping') {
|
|
|
|
|
|
const token = this.getToken();
|
|
|
|
|
|
if (token) {
|
|
|
|
|
|
message.token = token;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-19 16:04:43 +08:00
|
|
|
|
const messageStr = JSON.stringify(message);
|
|
|
|
|
|
this.socket.send({
|
|
|
|
|
|
data: messageStr,
|
|
|
|
|
|
success: () => {
|
|
|
|
|
|
console.log('消息发送成功:', message);
|
|
|
|
|
|
},
|
|
|
|
|
|
fail: (err) => {
|
|
|
|
|
|
console.error('消息发送失败:', err);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error('发送消息出错:', err);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addMessageHandler(handler) {
|
|
|
|
|
|
this.messageHandlers.push(handler);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
removeMessageHandler(handler) {
|
|
|
|
|
|
const index = this.messageHandlers.indexOf(handler);
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
this.messageHandlers.splice(index, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-30 10:04:30 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取用户ID并绑定客户端(携带token)
|
|
|
|
|
|
* @returns {string|null} 用户ID
|
|
|
|
|
|
*/
|
2025-08-29 16:59:14 +08:00
|
|
|
|
getUserIdAndBindClientId() {
|
|
|
|
|
|
const userId = uni.getStorageSync('user_id'); // 实际项目中从存储获取
|
2025-12-30 10:04:30 +08:00
|
|
|
|
const token = this.getToken();
|
|
|
|
|
|
|
2025-08-29 16:59:14 +08:00
|
|
|
|
if (userId == null || userId === 'user-' || userId === '') {
|
2025-12-30 10:04:30 +08:00
|
|
|
|
// 等待10秒后重试
|
2025-08-29 16:59:14 +08:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.getUserIdAndBindClientId();
|
|
|
|
|
|
}, 10000)
|
2025-12-30 10:04:30 +08:00
|
|
|
|
} else if (!token) {
|
|
|
|
|
|
// token不存在,等待后重试
|
|
|
|
|
|
console.warn('token不存在,等待后重试绑定');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.getUserIdAndBindClientId();
|
|
|
|
|
|
}, 5000)
|
2025-08-29 16:59:14 +08:00
|
|
|
|
} else {
|
2026-02-07 11:12:18 +08:00
|
|
|
|
// 直接发送bind请求,Go后端会自动清理旧连接
|
|
|
|
|
|
// 注意:不在这里调用unbind,因为unbind会关闭当前连接
|
2025-07-19 16:04:43 +08:00
|
|
|
|
const bindMessage = {
|
|
|
|
|
|
request_type: 'bind',
|
|
|
|
|
|
user_type: 'user',
|
|
|
|
|
|
sender_user_id: `user-${userId}`,
|
2026-02-07 11:12:18 +08:00
|
|
|
|
token: token,
|
|
|
|
|
|
clean_old_connections: true // 告诉Go后端清理旧连接
|
2025-07-19 16:04:43 +08:00
|
|
|
|
};
|
|
|
|
|
|
this.send(bindMessage);
|
|
|
|
|
|
}
|
2025-08-29 16:59:14 +08:00
|
|
|
|
return userId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleMessage(data) {
|
2025-12-30 10:04:30 +08:00
|
|
|
|
// 处理认证响应
|
|
|
|
|
|
if (data.auth_status !== undefined) {
|
|
|
|
|
|
if (data.auth_status === 'success') {
|
|
|
|
|
|
console.log('WebSocket认证成功');
|
|
|
|
|
|
this.isAuthenticated = true;
|
|
|
|
|
|
} else if (data.auth_status === 'failed') {
|
|
|
|
|
|
console.error('WebSocket认证失败:', data.message);
|
|
|
|
|
|
this.isAuthenticated = false;
|
|
|
|
|
|
// 清除缓存的token
|
|
|
|
|
|
this.cachedToken = null;
|
|
|
|
|
|
} else if (data.auth_status === 'token_expired') {
|
|
|
|
|
|
console.warn('Token已过期,需要重新认证');
|
|
|
|
|
|
this.isAuthenticated = false;
|
|
|
|
|
|
this.cachedToken = null;
|
|
|
|
|
|
// 通知应用层token过期
|
|
|
|
|
|
uni.$emit('ws-token-expired');
|
2026-02-07 11:12:18 +08:00
|
|
|
|
// 尝试重新绑定(会自动清理旧连接)
|
2025-12-30 10:04:30 +08:00
|
|
|
|
this.getUserIdAndBindClientId();
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-08-29 16:59:14 +08:00
|
|
|
|
|
2025-12-30 10:04:30 +08:00
|
|
|
|
// 如果对象中有clientId则给websocket发送绑定请求
|
|
|
|
|
|
if (data.clientId) {
|
2025-08-29 16:59:14 +08:00
|
|
|
|
this.getUserIdAndBindClientId(data.clientId);
|
|
|
|
|
|
}
|
2025-07-19 16:04:43 +08:00
|
|
|
|
|
2025-12-30 10:04:30 +08:00
|
|
|
|
// 处理聊天消息或其他消息
|
|
|
|
|
|
this.messageHandlers.forEach(handler => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
handler(data);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('消息处理出错:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2025-07-19 16:04:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
startHeartbeat() {
|
|
|
|
|
|
this.pingInterval = setInterval(() => {
|
|
|
|
|
|
if (this.isConnected) {
|
|
|
|
|
|
this.send({type: 'ping'});
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 300000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stopHeartbeat() {
|
|
|
|
|
|
if (this.pingInterval) {
|
|
|
|
|
|
clearInterval(this.pingInterval);
|
|
|
|
|
|
this.pingInterval = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
handleReconnect() {
|
|
|
|
|
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
|
|
|
|
console.log('已达到最大重连次数');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
this.reconnectAttempts++;
|
|
|
|
|
|
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000);
|
|
|
|
|
|
console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
|
|
|
|
|
this.connect();
|
|
|
|
|
|
}, this.reconnectDelay);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
disconnect() {
|
|
|
|
|
|
if (this.socket) {
|
|
|
|
|
|
this.socket.close();
|
|
|
|
|
|
this.socket = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
this.isConnected = false;
|
|
|
|
|
|
this.stopHeartbeat();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 全局WebSocket实例
|
2026-01-09 13:58:09 +08:00
|
|
|
|
// export const webSocketManager = new WebSocketManager('ws://127.0.0.1:12080/ws');
|
2026-07-19 16:08:04 +08:00
|
|
|
|
// const wsUrl = checkDev('dev')? 'ws://127.0.0.1:12080/ws':'wss://api.ws.g.xiaokang88.com/ws';
|
|
|
|
|
|
const wsUrl = checkDev('dev')? 'ws://127.0.0.1:12080/ws':'wss://xk.ws.nailaoyun.cn/ws';
|
2026-01-09 13:58:09 +08:00
|
|
|
|
export const webSocketManager = new WebSocketManager(wsUrl);
|