useWebSocket.js 5.9 KB

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