feat(sdk): 升级HaveADrink SDK并集成邀请系统
- 将HaveADrink依赖从1.0.8升级至1.0.12版本 - 集成邀请码功能,新增common/invite.js处理邀请链路 - 实现发送好友请求返回邀请码的新流程 - 添加删除动态功能,新增DeleteFeed接口 - 集成UniAppWebSocket适配器,重构WebSocket连接管理 - 优化好友请求支持关键词搜索功能 - 在FeedCard组件中添加删除按钮和分享按钮 - 更新SDK类型定义文件以匹配新接口规范
This commit is contained in:
+115
-145
@@ -1,10 +1,34 @@
|
||||
/* 碰盏日记 - WebSocket 管理器
|
||||
* 单例模式,负责私信实时通信
|
||||
* 协议格式与 HaveADrink SDK ChatWebSocket 一致:{ mesgType, data }
|
||||
* 支持心跳、自动重连、消息队列
|
||||
* 基于 HaveADrink SDK 的 ChatWebSocket(ChatWebSocketConn)实现:
|
||||
* - 底层 socket 通过 UniAppWebSocket 适配器注入(common/uni-websocket.js)
|
||||
* - 连接地址、token query、指数退避重连均由 SDK 管理
|
||||
* - 消息协议:{ mesgType, data }
|
||||
* 对外保持 on/off/send/connect/disconnect/userId 接口,页面层无感知
|
||||
*/
|
||||
|
||||
const WS_HOST = 'wss://dev.wash-painting.cn/api/have_a_drink/v1/chat/chat'
|
||||
import client, { API_HOST } from './api'
|
||||
// SDK 具名导出的连接类(绕开 client.ChatWebSocket 方法内部使用的浏览器 API)
|
||||
import { ChatWebSocketConn } from 'HaveADrink'
|
||||
|
||||
/**
|
||||
* 自行解析 host 并构造 ChatWebSocketConn
|
||||
* SDK 的 client.ChatWebSocket() 内部使用 new URL() / new URLSearchParams(),
|
||||
* 微信小程序沙箱中裸标识符 URL 解析到不可构造的空壳,全局 polyfill 无法生效,
|
||||
* 因此这里手动完成同样的逻辑:https -> wss,query 键值对拼接
|
||||
* @param {object} input - { token }
|
||||
* @returns {ChatWebSocketConn}
|
||||
*/
|
||||
function createChatConn(input) {
|
||||
const m = String(API_HOST).match(/^(https?|wss?):\/\/([^/?#\s]+)/i)
|
||||
const scheme = m && m[1].toLowerCase() === 'https' ? 'wss' : 'ws'
|
||||
const host = `${scheme}://${m ? m[2] : API_HOST}`
|
||||
const query = Object.keys(input || {})
|
||||
.filter((k) => input[k] !== '' && input[k] !== null && input[k] !== undefined)
|
||||
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(input[k])}`)
|
||||
.join('&')
|
||||
return new ChatWebSocketConn(host, query)
|
||||
}
|
||||
|
||||
// 客户端消息类型(与 SDK ClientMesgType 枚举值一致)
|
||||
const ClientMesgType = {
|
||||
@@ -14,42 +38,26 @@ const ClientMesgType = {
|
||||
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.conn = null // SDK ChatWebSocketConn 实例
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
this.listeners = {} // 事件订阅 { event: [callbacks] }
|
||||
this.messageQueue = [] // 断线期间暂存消息
|
||||
this.messageQueue = [] // 断线期间暂存的消息 { mesgType, data }
|
||||
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
|
||||
this.userId = '' // 服务端 connected 事件返回的用户ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立 WebSocket 连接
|
||||
* 建立 WebSocket 连接(走 SDK ChatWebSocket)
|
||||
* @param {string} token - 用户认证 token
|
||||
*/
|
||||
connect(token) {
|
||||
if (this.isConnected || this.isConnecting) return
|
||||
if (this.conn || this.isConnecting) return
|
||||
this.token = token || uni.getStorageSync('auth_token') || ''
|
||||
if (!this.token) {
|
||||
console.warn('[WS] 无 token,跳过连接')
|
||||
@@ -57,65 +65,80 @@ class WebSocketManager {
|
||||
}
|
||||
this.manualClose = false
|
||||
this.isConnecting = true
|
||||
console.log('[WS] 正在连接(SDK ChatWebSocket)...')
|
||||
|
||||
const url = WS_HOST
|
||||
console.log('[WS] 正在连接...')
|
||||
// SDK 内部:new websocket(`${host}/api/have_a_drink/v1/chat/chat?token=xxx`)
|
||||
// websocket 即注入的 UniAppWebSocket 适配器,重连由 SDK 指数退避管理
|
||||
// 注意:不用 client.ChatWebSocket()(其内部 new URL 在小程序沙箱不可用),
|
||||
// 直接用 SDK 导出的 ChatWebSocketConn 构造,等价实现
|
||||
const conn = createChatConn({ token: this.token })
|
||||
|
||||
this.socketTask = uni.connectSocket({
|
||||
url,
|
||||
header: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
},
|
||||
complete: () => {}
|
||||
})
|
||||
|
||||
this.socketTask.onOpen(() => {
|
||||
conn.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(() => {
|
||||
conn.onclose = () => {
|
||||
console.log('[WS] 连接关闭')
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
this._stopHeartbeat()
|
||||
this._emit('disconnect', {})
|
||||
if (!this.manualClose) {
|
||||
this._scheduleReconnect()
|
||||
}
|
||||
})
|
||||
// 非主动关闭时,SDK 已自动安排指数退避重连,无需手动处理
|
||||
}
|
||||
|
||||
this.socketTask.onError((err) => {
|
||||
conn.onerror = (err) => {
|
||||
console.warn('[WS] 连接错误', err)
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
})
|
||||
}
|
||||
|
||||
// ===== 服务端协议消息(SDK 已解析 { mesgType, data }) =====
|
||||
conn.onServerChatData = (data) => {
|
||||
this._emit('message', data)
|
||||
}
|
||||
|
||||
conn.onServerTypingData = (data) => {
|
||||
this._emit('typing', data)
|
||||
}
|
||||
|
||||
conn.onServerReadData = (data) => {
|
||||
this._emit('read', data)
|
||||
}
|
||||
|
||||
conn.onServerAckData = (data) => {
|
||||
this._emit('ack', data)
|
||||
}
|
||||
|
||||
conn.onServerConnectedData = (data) => {
|
||||
// 服务端验证 token 成功后推送,携带 userId
|
||||
this.userId = data && data.userId ? data.userId : ''
|
||||
console.log('[WS] 服务端确认连接, userId:', this.userId)
|
||||
this._emit('connected', data)
|
||||
}
|
||||
|
||||
conn.onServerKickedData = (data) => {
|
||||
console.warn('[WS] 被踢下线')
|
||||
this._emit('kicked', data)
|
||||
this.disconnect()
|
||||
}
|
||||
|
||||
this.conn = conn
|
||||
}
|
||||
|
||||
/** 主动断开连接 */
|
||||
/** 主动断开连接(SDK close() 会关闭重连标志并断开 socket) */
|
||||
disconnect() {
|
||||
this.manualClose = true
|
||||
this._stopHeartbeat()
|
||||
this._clearReconnect()
|
||||
if (this.socketTask) {
|
||||
this.socketTask.close()
|
||||
this.socketTask = null
|
||||
if (this.conn) {
|
||||
this.conn.close()
|
||||
this.conn = null
|
||||
}
|
||||
this.isConnected = false
|
||||
this.isConnecting = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,17 +147,16 @@ class WebSocketManager {
|
||||
* @param {object} data - 消息数据
|
||||
*/
|
||||
send(mesgType, data = {}) {
|
||||
const payload = JSON.stringify({ mesgType, data })
|
||||
if (this.isConnected && this.socketTask) {
|
||||
this.socketTask.send({ data: payload })
|
||||
if (this.isConnected && this.conn) {
|
||||
this._write(mesgType, data)
|
||||
} else {
|
||||
// 断线暂存队列(ping 不暂存)
|
||||
if (mesgType !== ClientMesgType.Ping) {
|
||||
this.messageQueue.push(payload)
|
||||
this.messageQueue.push({ mesgType, data })
|
||||
}
|
||||
// 尝试重连
|
||||
if (!this.isConnecting && !this.manualClose) {
|
||||
this._scheduleReconnect()
|
||||
// 未连接时触发(重)连接
|
||||
if (!this.conn && !this.manualClose) {
|
||||
this.connect(this.token)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,38 +189,31 @@ class WebSocketManager {
|
||||
|
||||
// ========== 内部方法 ==========
|
||||
|
||||
/** 处理收到的消息(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)
|
||||
/** 通过 SDK writeClient* 方法写入消息 */
|
||||
_write(mesgType, data) {
|
||||
try {
|
||||
switch (mesgType) {
|
||||
case ClientMesgType.Chat:
|
||||
this.conn.writeClientChatData(data)
|
||||
break
|
||||
case ClientMesgType.Typing:
|
||||
this.conn.writeClientTypingData(data)
|
||||
break
|
||||
case ClientMesgType.Read:
|
||||
this.conn.writeClientReadData(data)
|
||||
break
|
||||
case ClientMesgType.Ping:
|
||||
this.conn.writeClientPingData(data)
|
||||
break
|
||||
default:
|
||||
console.warn('[WS] 未知发送类型', mesgType)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[WS] 发送异常', e)
|
||||
// 发送失败时暂存,待重连后补发
|
||||
if (mesgType !== ClientMesgType.Ping) {
|
||||
this.messageQueue.push({ mesgType, data })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,61 +227,20 @@ class WebSocketManager {
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动心跳 */
|
||||
/** 启动心跳(保活,防运营商/小程序后台掐连接) */
|
||||
_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
|
||||
}
|
||||
|
||||
/** 重连成功后重发队列消息 */
|
||||
@@ -275,11 +249,7 @@ class WebSocketManager {
|
||||
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 })
|
||||
}
|
||||
})
|
||||
queue.forEach(({ mesgType, data }) => this._write(mesgType, data))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user