feat(sdk): 升级HaveADrink SDK并集成邀请系统

- 将HaveADrink依赖从1.0.8升级至1.0.12版本
- 集成邀请码功能,新增common/invite.js处理邀请链路
- 实现发送好友请求返回邀请码的新流程
- 添加删除动态功能,新增DeleteFeed接口
- 集成UniAppWebSocket适配器,重构WebSocket连接管理
- 优化好友请求支持关键词搜索功能
- 在FeedCard组件中添加删除按钮和分享按钮
- 更新SDK类型定义文件以匹配新接口规范
This commit is contained in:
cg
2026-08-04 23:32:45 +08:00
parent f988c8d868
commit 57eef74eb1
19 changed files with 1461 additions and 223 deletions
+115 -145
View File
@@ -1,10 +1,34 @@
/* 碰盏日记 - WebSocket 管理器
* 单例模式,负责私信实时通信
* 协议格式与 HaveADrink SDK ChatWebSocket 一致:{ mesgType, data }
* 支持心跳、自动重连、消息队列
* 基于 HaveADrink SDK ChatWebSocketChatWebSocketConn)实现:
* - 底层 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 -> 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 = {
@@ -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))
}
}