/** * 用户 WebSocket 单例:登录后全局一条连接,按 event 分发 * - device_data → appWs:deviceData(设备详情订阅读取) * - device_online_status → appWs:deviceOnlineStatus(设备列表等) * - notice → appWs:notice(通知 tab) * 与首页 DeviceTab 列表批量 subscribe / unsubscribe 及 deviceUserWs mixin 协议一致 */ // 与 deviceUserWs 同址,改一处即可 const WS_USER_URL = 'wss://eguo.chuantewulian.cn/prod-api/ws/user' // const WS_USER_URL = 'ws://192.168.1.5:4601/ws/user' let _task = null let _socketOpen = false let _reconnectTimer = null let _reconnectAttempts = 0 const _maxAttempts = 5 let _reconnectInterval = 3000 let _appActive = true function isMiniProgramPlatform() { try { if (typeof process !== 'undefined' && process.env && process.env.UNI_PLATFORM) { return String(process.env.UNI_PLATFORM).indexOf('mp-') === 0 } } catch (e) {} if (typeof __wxConfig !== 'undefined' && __wxConfig != null) { return true } return false } /** * 与 common/http.interceptor.js buildUrlWithLang 一致:长连接 query 用服务端约定的 locale(如中文为 zh_CN 而非 zh) */ function wsLangQueryValue(storedLang) { const s = String(storedLang || 'en') switch (s) { case 'ru': return 'ru_RU' case 'zh': return 'zh_CN' case 'ja': return 'ja_JP' case 'en': return 'en_US' default: return s } } function _buildUrl() { let url = WS_USER_URL const token = uni.getStorageSync('token') || '' const lang = wsLangQueryValue(uni.getStorageSync('language') || 'en') const q = [] if (token) { q.push('token=' + encodeURIComponent(token)) } q.push('lang=' + encodeURIComponent(lang)) return url + '?' + q.join('&') } function _getConnectOptions() { const url = _buildUrl() const token = uni.getStorageSync('token') || '' if (isMiniProgramPlatform()) { return { url } } return { url, header: { 'content-type': 'application/json;charset=UTF-8', Authorization: token, }, } } function _isTaskReady() { if (!_task || typeof _task.send !== 'function') return false if (typeof _task.readyState === 'number') { return _task.readyState === 1 } if (typeof _task.readyState === 'string') { return String(_task.readyState).toUpperCase() === 'OPEN' } return false } function _clearReconnectTimer() { if (_reconnectTimer != null) { clearTimeout(_reconnectTimer) _reconnectTimer = null } } function _scheduleReconnect() { if (!_appActive) return if (!uni.getStorageSync('token')) return if (_reconnectAttempts >= _maxAttempts) { console.log('[appUserWs] 已达最大重连次数,停止') return } _reconnectAttempts++ const delay = _reconnectInterval console.log(`[appUserWs] 第 ${_reconnectAttempts} 次重连,约 ${delay / 1000}s 后…`) _clearReconnectTimer() _reconnectTimer = setTimeout(() => { _reconnectTimer = null connectIfLoggedIn(true) }, delay) _reconnectInterval = Math.min(_reconnectInterval * 2, 30000) } function _onMessage(res) { let msg try { const raw = res && res.data !== undefined && res.data !== null ? res.data : res msg = typeof raw === 'string' ? JSON.parse(raw) : raw } catch (e) { console.log('[appUserWs] 消息解析失败', res) return } if (!msg || typeof msg !== 'object') return const event = msg.event if (event === 'device_data') { uni.$emit('appWs:deviceData', msg) return } if (event === 'device_online_status') { uni.$emit('appWs:deviceOnlineStatus', msg) return } if (event === 'notice') { uni.$emit('appWs:notice', msg) return } console.log('[appUserWs] 未处理 event:', event, msg) } function _doConnect(isReconnect) { if (!uni.getStorageSync('token')) { return } _clearReconnectTimer() if (_task) { try { _task.close({}) } catch (e) {} _task = null } _socketOpen = false const opts = _getConnectOptions() console.log('[appUserWs] 连接中…', opts.url) _task = uni.connectSocket({ ...opts, success: () => {}, fail: (err) => { console.error('[appUserWs] connectSocket 失败', err) _task = null _socketOpen = false _scheduleReconnect() }, }) if (!_task || !_task.onOpen) return // 用闭包绑定当前条连接:切换语言/重连时旧 socket 的 onClose 晚于新 socket 创建,否则会误把 _task 置空并打挂新线 const sock = _task _task.onOpen(() => { if (_task !== sock) return _reconnectAttempts = 0 _reconnectInterval = 3000 _socketOpen = true console.log('[appUserWs] 已打开', opts.url) uni.$emit('appWs:opened', {}) }) _task.onMessage(_onMessage) _task.onError((err) => { if (_task !== sock) return console.error('[appUserWs] onError', err) _socketOpen = false _scheduleReconnect() }) _task.onClose((res) => { if (_task !== sock) { // 旧线关闭,主连接已交给新 task,不清理、不重连 return } console.log('[appUserWs] onClose', res) _socketOpen = false _task = null if (_appActive && uni.getStorageSync('token')) { _scheduleReconnect() } }) } /** * 有 token 时建立/保持连接;无 token 时断开 * @param {boolean} [forceReconnect] 为 true 时强制走完整建连(重连场景) */ export function connectIfLoggedIn(forceReconnect) { if (!uni.getStorageSync('token')) { disconnect() return } _appActive = true // 已连接时直接返回,勿 emit appWs:opened,否则监听方会误以为是「新连接」反复触发表层逻辑(如列表多次退订/订阅) if (!forceReconnect && _socketOpen && _task && _isTaskReady()) { return } _doConnect() } export function disconnect() { _appActive = false _clearReconnectTimer() _reconnectAttempts = 0 _reconnectInterval = 3000 _socketOpen = false if (_task) { try { _task.close({ success: () => {}, fail: () => {} }) } catch (e) {} _task = null } } export function isOpen() { return _socketOpen && _isTaskReady() } /** * 发送 JSON(subscribe / unsubscribe 等) */ export function sendJson(obj, label) { if (!_isTaskReady()) { console.warn('[appUserWs] 未就绪,跳过发送', label, obj) return } try { _task.send({ data: JSON.stringify(obj), success: () => { if (label) console.log('[appUserWs] 发送', label, obj) }, fail: (err) => { console.error('[appUserWs] 发送失败', err, obj) }, }) } catch (e) { console.error('[appUserWs] send 异常', e) } } export function setAppActive(active) { _appActive = active !== false } /** * 语言变更后重连:URL 的 lang 与 Storage 一致,需完整断开再建连,请保证先 setStorageSync('language', …) * 与 connectIfLoggedIn(true) 等价,命名便于在 i18n 等处明确语义 */ export function reconnectUserWsForLanguage() { connectIfLoggedIn(true) }