Files
hejiu/common/websocket.js
T
cg c7c023975d feat(circle): 集成HaveADrink SDK实现酒友圈功能
- 集成HaveADrink SDK版本从1.0.4升级至1.0.8
- 实现酒友圈API接口,替换原有的mock数据
- 添加地理位置权限配置(requiredPrivateInfos)
- 重构API调用方式,统一使用client实例调用真实接口
- 更新WebSocket协议与SDK保持一致,支持聊天、输入状态、已读回执等功能
- 实现游标分页获取动态信息流
- 添加文件上传API支持图片上传
- 优化API错误处理,支持业务级错误提示
- 调整酒局状态显示,新增upcoming待开始状态
- 优化自我感觉标签映射逻辑,支持更多状态类型
- 更新websocket连接认证方式,使用Authorization header传递token
- 添加服务端连接确认和被踢下线事件处理
2026-07-22 23:03:40 +08:00

289 lines
7.6 KiB
JavaScript

/* 碰盏日记 - WebSocket 管理器
* 单例模式,负责私信实时通信
* 协议格式与 HaveADrink SDK ChatWebSocket 一致:{ mesgType, data }
* 支持心跳、自动重连、消息队列
*/
const WS_HOST = 'wss://dev.wash-painting.cn/api/have_a_drink/v1/chat/chat'
// 客户端消息类型(与 SDK ClientMesgType 枚举值一致)
const ClientMesgType = {
Chat: 'chat',
Typing: 'typing',
Read: 'read',
Ping: 'ping'
}
// 服务端消息类型(与 SDK ServerMesgType 枚举值一致)
const ServerMesgType = {
Chat: 'chat',
Typing: 'typing',
Read: 'read',
Ack: 'ack',
Pong: 'pong',
Connected: 'connected',
Kicked: 'kicked'
}
class WebSocketManager {
constructor() {
this.socketTask = null
this.isConnected = false
this.isConnecting = false
this.listeners = {} // 事件订阅 { event: [callbacks] }
this.messageQueue = [] // 断线期间暂存消息
this.heartbeatTimer = null
this.heartbeatTimeout = null
this.reconnectTimer = null
this.reconnectAttempts = 0
this.maxReconnectDelay = 30000
this.heartbeatInterval = 30000 // 30s 发一次 ping
this.heartbeatWait = 60000 // 60s 无 pong 则重连
this.manualClose = false
this.token = ''
this.userId = '' // 连接成功后服务端返回的用户ID
}
/**
* 建立 WebSocket 连接
* @param {string} token - 用户认证 token
*/
connect(token) {
if (this.isConnected || this.isConnecting) return
this.token = token || uni.getStorageSync('auth_token') || ''
if (!this.token) {
console.warn('[WS] 无 token,跳过连接')
return
}
this.manualClose = false
this.isConnecting = true
const url = WS_HOST
console.log('[WS] 正在连接...')
this.socketTask = uni.connectSocket({
url,
header: {
'Authorization': `Bearer ${this.token}`
},
complete: () => {}
})
this.socketTask.onOpen(() => {
console.log('[WS] 连接成功')
this.isConnected = true
this.isConnecting = false
this.reconnectAttempts = 0
this._startHeartbeat()
this._flushQueue()
this._emit('connect', {})
})
this.socketTask.onMessage((res) => {
try {
const msg = JSON.parse(res.data)
this._handleMessage(msg)
} catch (e) {
console.warn('[WS] 消息解析失败', res.data)
}
})
this.socketTask.onClose(() => {
console.log('[WS] 连接关闭')
this.isConnected = false
this.isConnecting = false
this._stopHeartbeat()
this._emit('disconnect', {})
if (!this.manualClose) {
this._scheduleReconnect()
}
})
this.socketTask.onError((err) => {
console.warn('[WS] 连接错误', err)
this.isConnected = false
this.isConnecting = false
})
}
/** 主动断开连接 */
disconnect() {
this.manualClose = true
this._stopHeartbeat()
this._clearReconnect()
if (this.socketTask) {
this.socketTask.close()
this.socketTask = null
}
this.isConnected = false
}
/**
* 发送消息(与 SDK writeClient* 方法格式一致)
* @param {string} mesgType - 消息类型: chat | typing | read | ping
* @param {object} data - 消息数据
*/
send(mesgType, data = {}) {
const payload = JSON.stringify({ mesgType, data })
if (this.isConnected && this.socketTask) {
this.socketTask.send({ data: payload })
} else {
// 断线暂存队列(ping 不暂存)
if (mesgType !== ClientMesgType.Ping) {
this.messageQueue.push(payload)
}
// 尝试重连
if (!this.isConnecting && !this.manualClose) {
this._scheduleReconnect()
}
}
}
/**
* 订阅事件
* @param {string} event - message | typing | read | ack | connected | kicked | connect | disconnect
* @param {function} callback
*/
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event].push(callback)
}
/**
* 取消订阅
* @param {string} event
* @param {function} callback
*/
off(event, callback) {
if (!this.listeners[event]) return
if (callback) {
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback)
} else {
delete this.listeners[event]
}
}
// ========== 内部方法 ==========
/** 处理收到的消息(SDK ServerMesgType 协议) */
_handleMessage(msg) {
const { mesgType, data } = msg
switch (mesgType) {
case ServerMesgType.Chat:
this._emit('message', data)
break
case ServerMesgType.Typing:
this._emit('typing', data)
break
case ServerMesgType.Read:
this._emit('read', data)
break
case ServerMesgType.Ack:
this._emit('ack', data)
break
case ServerMesgType.Pong:
this._onPong()
break
case ServerMesgType.Connected:
// 服务端验证 token 成功后推送,携带 userId
this.userId = data && data.userId ? data.userId : ''
console.log('[WS] 服务端确认连接, userId:', this.userId)
this._emit('connected', data)
break
case ServerMesgType.Kicked:
console.warn('[WS] 被踢下线')
this._emit('kicked', data)
this.disconnect()
break
default:
console.log('[WS] 未知消息类型', mesgType)
}
}
/** 触发事件 */
_emit(event, data) {
const cbs = this.listeners[event]
if (cbs) {
cbs.forEach(cb => {
try { cb(data) } catch (e) { console.warn('[WS] 事件回调异常', e) }
})
}
}
/** 启动心跳 */
_startHeartbeat() {
this._stopHeartbeat()
this.heartbeatTimer = setInterval(() => {
this.send(ClientMesgType.Ping, {})
// 设置超时检测
this.heartbeatTimeout = setTimeout(() => {
console.warn('[WS] 心跳超时,触发重连')
if (this.socketTask) {
this.socketTask.close()
}
}, this.heartbeatWait)
}, this.heartbeatInterval)
}
/** 收到 pong,清除超时 */
_onPong() {
if (this.heartbeatTimeout) {
clearTimeout(this.heartbeatTimeout)
this.heartbeatTimeout = null
}
}
/** 停止心跳 */
_stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
if (this.heartbeatTimeout) {
clearTimeout(this.heartbeatTimeout)
this.heartbeatTimeout = null
}
}
/** 指数退避重连 */
_scheduleReconnect() {
if (this.manualClose || this.reconnectTimer) return
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay)
this.reconnectAttempts++
console.log(`[WS] ${delay}ms 后尝试第 ${this.reconnectAttempts} 次重连`)
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null
this.isConnecting = false
this.connect(this.token)
}, delay)
}
/** 清除重连计时器 */
_clearReconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
this.reconnectAttempts = 0
}
/** 重连成功后重发队列消息 */
_flushQueue() {
if (!this.messageQueue.length) return
console.log(`[WS] 重发 ${this.messageQueue.length} 条暂存消息`)
const queue = [...this.messageQueue]
this.messageQueue = []
queue.forEach(payload => {
if (this.socketTask && this.isConnected) {
this.socketTask.send({ data: payload })
}
})
}
}
// 导出全局单例
const wsManager = new WebSocketManager()
export default wsManager