useWebSocket.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. /**
  2. * 消息 WebSocket:登录后连接,收到新消息时更新 chatStore(消息列表、本地未读、会话预览),不整表刷新
  3. */
  4. import { getToken, normalizeMessageContentType } from '../utils/api'
  5. import { chatStore } from '../store/chat'
  6. const WS_BASE = 'wss://api.hnyunzhu.com/api/v1/ws/messages'
  7. const HEARTBEAT_INTERVAL = 30000
  8. let socketTask = null
  9. let fetchContactsRef = null
  10. let heartbeatTimer = null
  11. function normalizeWsMessage(raw) {
  12. const m = raw.message || raw
  13. const type = m.type ?? 'MESSAGE'
  14. // 通知类消息:应用发给用户,当前用户始终为接收方
  15. const isMe = type === 'NOTIFICATION' ? false : (m.is_me ?? m.isMe)
  16. const rawText = m.content ?? m.text ?? ''
  17. const urlField = m.url ?? m.file_url ?? m.content_url
  18. const content =
  19. urlField && /^https?:\/\//i.test(String(urlField)) ? String(urlField) : String(rawText)
  20. return {
  21. id: String(m.id),
  22. type,
  23. senderId: m.sender_id ?? m.senderId,
  24. receiverId: m.receiver_id ?? m.receiverId,
  25. content,
  26. contentType: normalizeMessageContentType(m.content_type ?? m.contentType ?? 'TEXT'),
  27. title: m.title,
  28. createdAt: m.created_at ?? m.createdAt,
  29. isMe,
  30. actionUrl: m.action_url ?? m.actionUrl,
  31. actionText: m.action_text ?? m.actionText
  32. }
  33. }
  34. /**
  35. * 根据推送消息计算会话 ID:私信用 sender_id/receiver_id,通知用负的 app_id
  36. */
  37. function getContactIdFromMessage(msg, currentUserId) {
  38. const type = msg.type ?? 'MESSAGE'
  39. const appId = msg.app_id ?? msg.appId
  40. if (type === 'NOTIFICATION' && appId != null && appId !== '') {
  41. return -Math.abs(Number(appId))
  42. }
  43. const senderId = msg.sender_id ?? msg.senderId
  44. const receiverId = msg.receiver_id ?? msg.receiverId
  45. // 当前用户是接收方则会话对方是 sender_id,否则是 receiver_id
  46. if (String(receiverId) === String(currentUserId)) return senderId
  47. return receiverId
  48. }
  49. export function useWebSocket(fetchContacts) {
  50. fetchContactsRef = fetchContacts
  51. function connect() {
  52. const token = getToken()
  53. if (!token || socketTask) return
  54. const url = `${WS_BASE}?token=${encodeURIComponent(token)}`
  55. socketTask = uni.connectSocket({
  56. url,
  57. success: () => {}
  58. })
  59. uni.onSocketOpen(() => {
  60. console.log('[WS] messages connected')
  61. heartbeatTimer = setInterval(() => {
  62. try {
  63. if (socketTask) uni.sendSocketMessage({ data: 'ping' })
  64. } catch (e) {}
  65. }, HEARTBEAT_INTERVAL)
  66. })
  67. uni.onSocketMessage((res) => {
  68. try {
  69. // 心跳:服务端回复 pong
  70. if (res.data === 'pong') return
  71. const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data
  72. // 文档格式:{ type: 'NEW_MESSAGE', data: { id, sender_id, receiver_id, ... } }
  73. const msg = data.type === 'NEW_MESSAGE' ? data.data : (data.message ?? data)
  74. if (!msg) return
  75. const normalized = normalizeWsMessage({ message: msg })
  76. // 需要当前用户 id 判断会话方:若后端推送里带 current_user_id 用那个,否则用 receiver_id 判断
  77. const currentUserId = data.current_user_id ?? normalized.receiverId
  78. const contactId = getContactIdFromMessage(msg, currentUserId)
  79. if (contactId != null) {
  80. const cid = String(contactId)
  81. const list = chatStore.messages[cid] || []
  82. const hasId = normalized.id && list.some((m) => String(m.id) === String(normalized.id))
  83. if (hasId) return
  84. chatStore.appendMessage(cid, normalized)
  85. // 前台 & 后台消息通知:若当前不在该会话,则给出提示
  86. const isActive = String(chatStore.activeContactId || '') === String(contactId)
  87. // 别人发给我且不在当前会话:本地未读 +1,并更新底部消息 Tab 角标
  88. if (!normalized.isMe && !isActive) {
  89. chatStore.incrementUnread(cid)
  90. chatStore.updateTabBarUnreadBadge()
  91. }
  92. if (!isActive) {
  93. const contact = (chatStore.contacts || []).find(
  94. (c) => String(c.user_id || c.id) === String(contactId)
  95. )
  96. const title = (contact && contact.title) || '新消息'
  97. let body = ''
  98. if (normalized.contentType === 'TEXT') {
  99. body = normalized.content ? String(normalized.content).slice(0, 50) : ''
  100. } else if (normalized.contentType === 'IMAGE') {
  101. body = '[图片]'
  102. } else if (normalized.contentType === 'VIDEO') {
  103. body = '[视频]'
  104. } else if (normalized.contentType === 'USER_NOTIFICATION') {
  105. body = normalized.title
  106. ? String(normalized.title).slice(0, 50)
  107. : normalized.content
  108. ? String(normalized.content).slice(0, 50)
  109. : '[通知]'
  110. } else {
  111. body = normalized.title || '[文件]'
  112. }
  113. // #ifdef APP-PLUS
  114. try {
  115. plus.push.createMessage(body || '您有一条新消息', { contactId }, { title })
  116. } catch (e) {
  117. // 兜底为 Toast
  118. uni.showToast({ title: `${title}: ${body || '新消息'}`, icon: 'none' })
  119. }
  120. // #endif
  121. // #ifndef APP-PLUS
  122. uni.showToast({ title: `${title}: ${body || '新消息'}`, icon: 'none' })
  123. // #endif
  124. }
  125. // 只更新该会话在列表中的最后一条预览与时间,不整表刷新
  126. let preview = ''
  127. if (normalized.contentType === 'TEXT') {
  128. preview = normalized.content ? String(normalized.content).slice(0, 50) : ''
  129. } else if (normalized.contentType === 'IMAGE') preview = '[图片]'
  130. else if (normalized.contentType === 'VIDEO') preview = '[视频]'
  131. else if (normalized.contentType === 'USER_NOTIFICATION') {
  132. preview = normalized.title
  133. ? String(normalized.title).slice(0, 50)
  134. : normalized.content
  135. ? String(normalized.content).slice(0, 50)
  136. : '[通知]'
  137. } else preview = normalized.title || '[文件]'
  138. chatStore.updateContactPreview(cid, { lastMessage: preview, time: normalized.createdAt })
  139. }
  140. } catch (e) {
  141. console.warn('[WS] parse message error', e)
  142. }
  143. })
  144. uni.onSocketError((err) => {
  145. console.warn('[WS] error', err)
  146. })
  147. uni.onSocketClose(() => {
  148. if (heartbeatTimer) {
  149. clearInterval(heartbeatTimer)
  150. heartbeatTimer = null
  151. }
  152. socketTask = null
  153. })
  154. }
  155. function disconnect() {
  156. if (heartbeatTimer) {
  157. clearInterval(heartbeatTimer)
  158. heartbeatTimer = null
  159. }
  160. if (socketTask) {
  161. uni.closeSocket()
  162. socketTask = null
  163. }
  164. }
  165. return { connect, disconnect }
  166. }