| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- /**
- * 消息 WebSocket:登录后连接,收到新消息时更新 chatStore(消息列表、会话预览),不整表刷新
- * 未读角标由 GET /messages/unread-count 刷新,不在此本地累加
- * 断线后自动重连(指数退避);主动 disconnect 时不重连
- */
- import { getToken, markHistoryReadAll, normalizeMessageContentType } from '../utils/api'
- import { chatStore } from '../store/chat'
- import { fetchContactsList } from './useContacts'
- import { fetchUnreadCountAndUpdateTabBar } from './useUnreadBadge'
- const WS_BASE = 'wss://api.hnyunzhu.com/api/v1/ws/messages'
- const HEARTBEAT_INTERVAL = 30000
- const INITIAL_RECONNECT_DELAY = 1000
- const MAX_RECONNECT_DELAY = 30000
- let socketTask = null
- let heartbeatTimer = null
- /** 仅 uni.onSocket* 注册一次,避免每次 connect 叠加监听 */
- let socketListenersAttached = false
- let intentionalClose = false
- let reconnectTimer = null
- let reconnectAttempt = 0
- function clearReconnectTimer() {
- if (reconnectTimer) {
- clearTimeout(reconnectTimer)
- reconnectTimer = null
- }
- }
- /** WS 心跳成功或收到推送后:拉总未读 + 会话列表 */
- function syncInboxFromServer() {
- if (!getToken()) return
- Promise.all([fetchUnreadCountAndUpdateTabBar(), fetchContactsList()]).catch(() => {})
- }
- function scheduleReconnect() {
- if (intentionalClose) return
- if (!getToken()) return
- clearReconnectTimer()
- const delay = Math.min(
- INITIAL_RECONNECT_DELAY * Math.pow(2, reconnectAttempt),
- MAX_RECONNECT_DELAY
- )
- reconnectAttempt += 1
- console.log(`[WS] reconnect scheduled in ${delay}ms (attempt ${reconnectAttempt})`)
- reconnectTimer = setTimeout(() => {
- reconnectTimer = null
- tryConnect()
- }, delay)
- }
- function normalizeWsMessage(raw) {
- const m = raw.message || raw
- const type = m.type ?? 'MESSAGE'
- // 通知类消息:应用发给用户,当前用户始终为接收方
- const isMe = type === 'NOTIFICATION' ? false : (m.is_me ?? m.isMe)
- const rawText = m.content ?? m.text ?? ''
- const urlField = m.url ?? m.file_url ?? m.content_url
- const content =
- urlField && /^https?:\/\//i.test(String(urlField)) ? String(urlField) : String(rawText)
- return {
- id: String(m.id),
- type,
- senderId: m.sender_id ?? m.senderId,
- receiverId: m.receiver_id ?? m.receiverId,
- content,
- contentType: normalizeMessageContentType(m.content_type ?? m.contentType ?? 'TEXT'),
- title: m.title,
- createdAt: m.created_at ?? m.createdAt,
- isMe,
- actionUrl: m.action_url ?? m.actionUrl,
- actionText: m.action_text ?? m.actionText
- }
- }
- /**
- * 根据推送消息计算会话 ID:私信用 sender_id/receiver_id,通知用负的 app_id
- */
- function getContactIdFromMessage(msg, currentUserId) {
- const type = msg.type ?? 'MESSAGE'
- const appId = msg.app_id ?? msg.appId
- if (type === 'NOTIFICATION' && appId != null && appId !== '') {
- return -Math.abs(Number(appId))
- }
- const senderId = msg.sender_id ?? msg.senderId
- const receiverId = msg.receiver_id ?? msg.receiverId
- // 当前用户是接收方则会话对方是 sender_id,否则是 receiver_id
- if (String(receiverId) === String(currentUserId)) return senderId
- return receiverId
- }
- function attachSocketListenersOnce() {
- if (socketListenersAttached) return
- socketListenersAttached = true
- uni.onSocketOpen(() => {
- reconnectAttempt = 0
- clearReconnectTimer()
- console.log('[WS] messages connected')
- syncInboxFromServer()
- heartbeatTimer = setInterval(() => {
- try {
- if (socketTask) uni.sendSocketMessage({ data: 'ping' })
- } catch (e) {}
- }, HEARTBEAT_INTERVAL)
- })
- uni.onSocketMessage((res) => {
- try {
- // 心跳:服务端回复 pong
- if (res.data === 'pong') {
- syncInboxFromServer()
- return
- }
- const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
- console.log('[WS] recv', data)
- syncInboxFromServer()
- // 文档格式:{ type: 'NEW_MESSAGE', data: { id, sender_id, receiver_id, ... } }
- const msg = data.type === 'NEW_MESSAGE' ? data.data : (data.message ?? data)
- if (!msg) {
- console.log('[WS] recv (ignored: no message payload)')
- return
- }
- const normalized = normalizeWsMessage({ message: msg })
- // 需要当前用户 id 判断会话方:若后端推送里带 current_user_id 用那个,否则用 receiver_id 判断
- const currentUserId = data.current_user_id ?? normalized.receiverId
- const contactId = getContactIdFromMessage(msg, currentUserId)
- if (contactId == null) {
- console.log('[WS] recv (ignored: no contactId)', { msg, currentUserId })
- return
- }
- {
- const cid = String(contactId)
- const list = chatStore.messages[cid] || []
- const hasId = normalized.id && list.some((m) => String(m.id) === String(normalized.id))
- if (hasId) {
- console.log('[WS] recv (ignored: duplicate id)', String(normalized.id))
- return
- }
- chatStore.appendMessage(cid, normalized)
- // 前台 & 后台消息通知:若当前不在该会话,则给出提示
- const isActive = String(chatStore.activeContactId || '') === String(contactId)
- if (!isActive) {
- const contact = (chatStore.contacts || []).find(
- (c) => String(c.user_id || c.id) === String(contactId)
- )
- const title = (contact && contact.title) || '新消息'
- let body = ''
- if (normalized.contentType === 'TEXT') {
- body = normalized.content ? String(normalized.content).slice(0, 50) : ''
- } else if (normalized.contentType === 'IMAGE') {
- body = '[图片]'
- } else if (normalized.contentType === 'VIDEO') {
- body = '[视频]'
- } else if (normalized.contentType === 'USER_NOTIFICATION') {
- body = normalized.title
- ? String(normalized.title).slice(0, 50)
- : normalized.content
- ? String(normalized.content).slice(0, 50)
- : '[通知]'
- } else {
- body = normalized.title || '[文件]'
- }
- // #ifdef APP-PLUS
- try {
- plus.push.createMessage(body || '您有一条新消息', { contactId }, { title })
- } catch (e) {
- // 兜底为 Toast
- uni.showToast({ title: `${title}: ${body || '新消息'}`, icon: 'none' })
- }
- // #endif
- // #ifndef APP-PLUS
- uni.showToast({ title: `${title}: ${body || '新消息'}`, icon: 'none' })
- // #endif
- } else {
- // 正在该会话对话框内收到新消息:标记本会话全部已读,并刷新未读与列表
- const t = getToken()
- if (t) {
- Promise.resolve(markHistoryReadAll(t, cid))
- .then(() =>
- Promise.all([fetchUnreadCountAndUpdateTabBar(), fetchContactsList()])
- )
- .catch(() => {})
- }
- }
- // 只更新该会话在列表中的最后一条预览与时间,不整表刷新
- let preview = ''
- if (normalized.contentType === 'TEXT') {
- preview = normalized.content ? String(normalized.content).slice(0, 50) : ''
- } else if (normalized.contentType === 'IMAGE') preview = '[图片]'
- else if (normalized.contentType === 'VIDEO') preview = '[视频]'
- else if (normalized.contentType === 'USER_NOTIFICATION') {
- preview = normalized.title
- ? String(normalized.title).slice(0, 50)
- : normalized.content
- ? String(normalized.content).slice(0, 50)
- : '[通知]'
- } else preview = normalized.title || '[文件]'
- chatStore.updateContactPreview(cid, { lastMessage: preview, time: normalized.createdAt })
- }
- } catch (e) {
- console.warn('[WS] parse message error', e, res && res.data)
- }
- })
- uni.onSocketError((err) => {
- console.warn('[WS] error', err)
- })
- uni.onSocketClose(() => {
- if (heartbeatTimer) {
- clearInterval(heartbeatTimer)
- heartbeatTimer = null
- }
- socketTask = null
- if (!intentionalClose && getToken()) {
- scheduleReconnect()
- }
- })
- }
- function tryConnect() {
- const token = getToken()
- if (!token || socketTask) return
- intentionalClose = false
- attachSocketListenersOnce()
- const url = `${WS_BASE}?token=${encodeURIComponent(token)}`
- socketTask = uni.connectSocket({
- url,
- success: () => {}
- })
- }
- function performDisconnect() {
- intentionalClose = true
- clearReconnectTimer()
- reconnectAttempt = 0
- if (heartbeatTimer) {
- clearInterval(heartbeatTimer)
- heartbeatTimer = null
- }
- if (socketTask) {
- uni.closeSocket()
- socketTask = null
- }
- }
- /**
- * 登出 / 清空 token 时关闭连接并禁止自动重连(供 setToken 等统一调用)
- */
- export function disconnectWebSocket() {
- performDisconnect()
- }
- /** 启动已登录 / 登录成功后显式建连(与 useWebSocket().connect 相同) */
- export function connectWebSocket() {
- tryConnect()
- }
- export function useWebSocket() {
- function connect() {
- tryConnect()
- }
- function disconnect() {
- performDisconnect()
- }
- return { connect, disconnect }
- }
|