Files
cg 57eef74eb1 feat(sdk): 升级HaveADrink SDK并集成邀请系统
- 将HaveADrink依赖从1.0.8升级至1.0.12版本
- 集成邀请码功能,新增common/invite.js处理邀请链路
- 实现发送好友请求返回邀请码的新流程
- 添加删除动态功能,新增DeleteFeed接口
- 集成UniAppWebSocket适配器,重构WebSocket连接管理
- 优化好友请求支持关键词搜索功能
- 在FeedCard组件中添加删除按钮和分享按钮
- 更新SDK类型定义文件以匹配新接口规范
2026-08-04 23:32:45 +08:00

259 lines
7.8 KiB
JavaScript
Raw Permalink 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.
/* 碰盏日记 - WebSocket 管理器
* 单例模式,负责私信实时通信
* 基于 HaveADrink SDK 的 ChatWebSocketChatWebSocketConn)实现:
* - 底层 socket 通过 UniAppWebSocket 适配器注入(common/uni-websocket.js
* - 连接地址、token query、指数退避重连均由 SDK 管理
* - 消息协议:{ mesgType, data }
* 对外保持 on/off/send/connect/disconnect/userId 接口,页面层无感知
*/
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 -> wssquery 键值对拼接
* @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 = {
Chat: 'chat',
Typing: 'typing',
Read: 'read',
Ping: 'ping'
}
class WebSocketManager {
constructor() {
this.conn = null // SDK ChatWebSocketConn 实例
this.isConnected = false
this.isConnecting = false
this.listeners = {} // 事件订阅 { event: [callbacks] }
this.messageQueue = [] // 断线期间暂存的消息 { mesgType, data }
this.heartbeatTimer = null
this.heartbeatInterval = 30000 // 30s 发一次 ping
this.manualClose = false
this.token = ''
this.userId = '' // 服务端 connected 事件返回的用户ID
}
/**
* 建立 WebSocket 连接(走 SDK ChatWebSocket
* @param {string} token - 用户认证 token
*/
connect(token) {
if (this.conn || this.isConnecting) return
this.token = token || uni.getStorageSync('auth_token') || ''
if (!this.token) {
console.warn('[WS] 无 token,跳过连接')
return
}
this.manualClose = false
this.isConnecting = true
console.log('[WS] 正在连接(SDK ChatWebSocket...')
// 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 })
conn.onopen = () => {
console.log('[WS] 连接成功')
this.isConnected = true
this.isConnecting = false
this._startHeartbeat()
this._flushQueue()
this._emit('connect', {})
}
conn.onclose = () => {
console.log('[WS] 连接关闭')
this.isConnected = false
this.isConnecting = false
this._stopHeartbeat()
this._emit('disconnect', {})
// 非主动关闭时,SDK 已自动安排指数退避重连,无需手动处理
}
conn.onerror = (err) => {
console.warn('[WS] 连接错误', err)
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()
if (this.conn) {
this.conn.close()
this.conn = null
}
this.isConnected = false
this.isConnecting = false
}
/**
* 发送消息(与 SDK writeClient* 方法格式一致)
* @param {string} mesgType - 消息类型: chat | typing | read | ping
* @param {object} data - 消息数据
*/
send(mesgType, data = {}) {
if (this.isConnected && this.conn) {
this._write(mesgType, data)
} else {
// 断线暂存队列(ping 不暂存)
if (mesgType !== ClientMesgType.Ping) {
this.messageQueue.push({ mesgType, data })
}
// 未连接时触发(重)连接
if (!this.conn && !this.manualClose) {
this.connect(this.token)
}
}
}
/**
* 订阅事件
* @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 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 })
}
}
}
/** 触发事件 */
_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.heartbeatInterval)
}
/** 停止心跳 */
_stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
/** 重连成功后重发队列消息 */
_flushQueue() {
if (!this.messageQueue.length) return
console.log(`[WS] 重发 ${this.messageQueue.length} 条暂存消息`)
const queue = [...this.messageQueue]
this.messageQueue = []
queue.forEach(({ mesgType, data }) => this._write(mesgType, data))
}
}
// 导出全局单例
const wsManager = new WebSocketManager()
export default wsManager