Files
xk-client-wx/utils/ws/websocket.js
李琦 2d2a8abfd4 feat(home/chat): 列表骨架屏、聊天室己方头像与协议/医生详情优化
- 新增分包组件 ListSkeleton(doctor/message/product/article/category/zone)
- 首页/消息/资讯首次加载改用骨架屏,刷新不再清空旧数据避免闪空
  · 去掉 home getList、消息 onShow 中的数组清空
  · 三页 pages.json 增加 easycom 与 componentPlaceholder 主包异步引用
- 聊天室己方头像改为读取本地 userinfo.avatarurl(登录后微信头像),
  mycollection 上传头像成功后同步写回 storage
- 修复协议抽屉点不开:page-container 改为 v-if + :show;
  协议名单独可点节点,匹配放宽 name/desc 双向 includes
- 医生气泡头像可点击进入 doctor-detail?readonly=1 只读医生详情,
  隐藏预约挂号区
2026-07-19 16:08:04 +08:00

336 lines
11 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.
import {checkDev} from "@/utils/utils";
export class WebSocketManager {
constructor(url) {
this.url = url;
this.socket = null;
this.isConnected = false;
this.isAuthenticated = false; // 是否已认证
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.messageHandlers = [];
this.pingInterval = null;
this.cachedToken = null; // 缓存的token
}
/**
* 获取当前用户的token
* @returns {string|null}
*/
getToken() {
// 从本地存储获取token不使用缓存确保获取最新token
let token = uni.getStorageSync('token');
if (token) {
// 移除Bearer前缀如果有
token = token.replace('Bearer ', '');
this.cachedToken = token;
}
return token || null;
}
/**
* 清除缓存的token登录后需要调用以获取最新token
*/
clearCachedToken() {
this.cachedToken = null;
}
/**
* 重新绑定用户(登录后调用)
* 会先清理旧连接,再绑定新连接
*/
rebindUser() {
const userId = uni.getStorageSync('user_id');
if (!userId) {
console.warn('重新绑定用户失败user_id不存在');
return false;
}
// 清除缓存的token确保获取最新token
this.clearCachedToken();
if (this.isConnected) {
// 如果已连接直接bindGo后端会自动清理旧连接
// 注意不在这里调用unbind因为unbind会关闭当前连接
return this.bindUser(userId, true);
} else {
// 如果未连接,先连接再绑定
this.connect();
// 连接成功后会在 onOpen 中自动绑定(会自动清理旧连接)
return true;
}
}
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();
// 绑定当前用户使用正确的keyuser_id
const userId = uni.getStorageSync('user_id');
if (userId) {
// 连接打开后立即绑定Go后端会自动清理旧连接
this.bindUser(userId, true);
}
});
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);
});
}
/**
* 解绑用户(清理旧连接)
* 注意:此方法会关闭当前连接,只应在需要完全断开时使用
* @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
}
/**
* 绑定用户携带token进行认证
* 在绑定前会自动清理旧连接通过Go后端的clean_old_connections功能
* @param {string} userId 用户ID可以是数字或user-xxx格式
* @param {boolean} cleanOldConnections 是否清理旧连接默认true
*/
bindUser(userId, cleanOldConnections = true) {
const token = this.getToken();
if (!token) {
console.error('绑定用户失败token不存在');
return false;
}
// 确保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);
return this.send({
request_type: 'bind',
user_type: 'user',
sender_user_id: normalizedUserId,
token: token,
clean_old_connections: cleanOldConnections // 告诉Go后端清理旧连接
});
}
/**
* 发送消息自动携带token
* @param {Object} message 消息对象
* @param {boolean} requireAuth 是否需要认证默认true
* @returns {boolean} 是否发送成功
*/
send(message, requireAuth = true) {
if (!this.isConnected) {
console.error('WebSocket未连接');
return false;
}
try {
// 如果需要认证且消息中没有token自动添加token
if (requireAuth && !message.token && message.request_type !== 'ping') {
const token = this.getToken();
if (token) {
message.token = token;
}
}
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);
}
}
/**
* 获取用户ID并绑定客户端携带token
* @returns {string|null} 用户ID
*/
getUserIdAndBindClientId() {
const userId = uni.getStorageSync('user_id'); // 实际项目中从存储获取
const token = this.getToken();
if (userId == null || userId === 'user-' || userId === '') {
// 等待10秒后重试
setTimeout(() => {
this.getUserIdAndBindClientId();
}, 10000)
} else if (!token) {
// token不存在等待后重试
console.warn('token不存在等待后重试绑定');
setTimeout(() => {
this.getUserIdAndBindClientId();
}, 5000)
} else {
// 直接发送bind请求Go后端会自动清理旧连接
// 注意不在这里调用unbind因为unbind会关闭当前连接
const bindMessage = {
request_type: 'bind',
user_type: 'user',
sender_user_id: `user-${userId}`,
token: token,
clean_old_connections: true // 告诉Go后端清理旧连接
};
this.send(bindMessage);
}
return userId;
}
handleMessage(data) {
// 处理认证响应
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');
// 尝试重新绑定(会自动清理旧连接)
this.getUserIdAndBindClientId();
}
return;
}
// 如果对象中有clientId则给websocket发送绑定请求
if (data.clientId) {
this.getUserIdAndBindClientId(data.clientId);
}
// 处理聊天消息或其他消息
this.messageHandlers.forEach(handler => {
try {
handler(data);
} catch (e) {
console.error('消息处理出错:', e);
}
});
}
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实例
// export const webSocketManager = new WebSocketManager('ws://127.0.0.1:12080/ws');
// 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';
export const webSocketManager = new WebSocketManager(wsUrl);